Skip to content

Commit

Permalink
Deferrals: set to audit instead of unenrolling (#2146)
Browse files Browse the repository at this point in the history
  • Loading branch information
annagav authored Apr 3, 2024
1 parent 0a8d37c commit a8bb61d
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 63 deletions.
45 changes: 29 additions & 16 deletions courses/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
ProgramCertificate,
ProgramEnrollment,
ProgramRequirement,
PaidCourseRun,
)
from courses.tasks import subscribe_edx_course_emails
from courses.utils import (
Expand Down Expand Up @@ -242,6 +243,7 @@ def create_run_enrollments(
user,
runs,
*,
change_status=None,
keep_failed_enrollments=False,
mode=EDX_DEFAULT_ENROLLMENT_MODE,
):
Expand All @@ -252,6 +254,7 @@ def create_run_enrollments(
Args:
user (User): The user to enroll
runs (iterable of CourseRun): The course runs to enroll in
change_status (str): The status of the enrollment
keep_failed_enrollments: (boolean): If True, keeps the local enrollment record
in the database even if the enrollment fails in edX.
mode (str): The course mode
Expand Down Expand Up @@ -315,7 +318,7 @@ def _enroll_learner_into_associated_programs():
user=user,
run=run,
defaults=dict(
change_status=None,
change_status=change_status,
edx_enrolled=edx_request_success,
enrollment_mode=mode,
),
Expand Down Expand Up @@ -499,6 +502,10 @@ def defer_enrollment(
from_enrollment = CourseRunEnrollment.all_objects.get(
user=user, run__courseware_id=from_courseware_id
)
downgraded_enrollments = []
already_deferred_from = (
from_enrollment.change_status == ENROLL_CHANGE_STATUS_DEFERRED
)
to_run = (
CourseRun.objects.get(courseware_id=to_courseware_id)
if to_courseware_id
Expand All @@ -509,7 +516,8 @@ def defer_enrollment(
downgraded_enrollments, _ = create_run_enrollments(
user=user,
runs=[from_enrollment.run],
keep_failed_enrollments=True,
change_status=ENROLL_CHANGE_STATUS_DEFERRED,
keep_failed_enrollments=keep_failed_enrollments,
mode=EDX_ENROLLMENT_AUDIT_MODE,
)
return downgraded_enrollments, None
Expand Down Expand Up @@ -538,19 +546,19 @@ def defer_enrollment(
from_enrollment.run.course.title, to_run.course.title
)
)
already_unenrolled_from = (
from_enrollment.change_status == ENROLL_CHANGE_STATUS_DEFERRED
)
if already_unenrolled_from:

if already_deferred_from:
# check if user was already enrolled in verified track
to_enrollments = CourseRunEnrollment.objects.filter(
user=user, run=to_run, enrollment_mode=EDX_ENROLLMENT_VERIFIED_MODE
).first()
return from_enrollment, to_enrollments
if to_enrollments:
return from_enrollment, to_enrollments

to_enrollments, enroll_success = create_run_enrollments(
user,
[to_run],
user=user,
runs=[to_run],
change_status=None,
keep_failed_enrollments=keep_failed_enrollments,
mode=EDX_ENROLLMENT_VERIFIED_MODE,
)
Expand All @@ -560,18 +568,23 @@ def defer_enrollment(
to_run
)
)
if not already_unenrolled_from:
from_enrollment = deactivate_run_enrollment(
from_enrollment,
ENROLL_CHANGE_STATUS_DEFERRED,
if not already_deferred_from:
downgraded_enrollments, enroll_success = create_run_enrollments(
user=user,
runs=[from_enrollment.run],
change_status=ENROLL_CHANGE_STATUS_DEFERRED,
keep_failed_enrollments=keep_failed_enrollments,
mode=EDX_ENROLLMENT_AUDIT_MODE,
)
if from_enrollment is None:
if not enroll_success and not keep_failed_enrollments:
raise Exception(
"Api call to deactivate enrollment on edX "
"Api call to change enrollment mode to audit on edX "
"was not successful for course run '{}'".format(from_courseware_id)
)
return from_enrollment, first_or_none(to_enrollments)
if PaidCourseRun.fulfilled_paid_course_run_exists(user, from_enrollment.run):
from_enrollment.change_payment_to_run(to_run)

return first_or_none(downgraded_enrollments), first_or_none(to_enrollments)


def ensure_course_run_grade(user, course_run, edx_grade, should_update=False):
Expand Down
129 changes: 83 additions & 46 deletions courses/api_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Courses API tests"""
from datetime import timedelta
from types import SimpleNamespace
from unittest.mock import Mock
from unittest.mock import Mock, call, patch

import factory
import pytest
Expand Down Expand Up @@ -56,6 +56,7 @@
ProgramEnrollment,
ProgramRequirement,
ProgramRequirementNodeType,
PaidCourseRun,
)
from ecommerce.factories import LineFactory, OrderFactory, ProductFactory
from ecommerce.models import Order
Expand Down Expand Up @@ -769,65 +770,81 @@ def test_deactivate_run_enrollment_line_does_not_exist(

@pytest.mark.parametrize("keep_failed_enrollments", [True, False])
@pytest.mark.parametrize("edx_enroll_succeeds", [True, False])
@pytest.mark.parametrize("edx_deactivate_succeeds", [True, False])
@pytest.mark.parametrize("edx_downgrade_succeeds", [True, False])
def test_defer_enrollment(
mocker,
course,
keep_failed_enrollments,
edx_enroll_succeeds,
edx_deactivate_succeeds,
edx_downgrade_succeeds,
):
"""
defer_enrollment should deactivate a user's existing enrollment and create an enrollment in another
course run
defer_enrollment should downgrade current enrollment to audit and create a verified enrollment in another
course run, and update PaidCourseRun to the new run
"""
course_runs = CourseRunFactory.create_batch(3, course=course)
existing_enrollment = CourseRunEnrollmentFactory.create(run=course_runs[0])
target_run = course_runs[1]
mock_new_enrollment = mocker.Mock()
patched_create_enrollments = mocker.patch(
"courses.api.create_run_enrollments",
autospec=True,
return_value=(
[mock_new_enrollment if edx_enroll_succeeds else None],
edx_enroll_succeeds,
),
fulfilled_order = OrderFactory.create(state=Order.STATE.FULFILLED)
paid_course_run = PaidCourseRun.objects.create(
user=existing_enrollment.user, course_run=course_runs[0], order=fulfilled_order
)
patched_deactivate_enrollments = mocker.patch(
"courses.api.deactivate_run_enrollment",
autospec=True,
return_value=existing_enrollment
if (keep_failed_enrollments or edx_deactivate_succeeds)
else None,
)
if keep_failed_enrollments or (edx_enroll_succeeds and edx_deactivate_succeeds):
returned_from_enrollment, returned_to_enrollment = defer_enrollment(
existing_enrollment.user,
existing_enrollment.run.courseware_id,
course_runs[1].courseware_id,
keep_failed_enrollments=keep_failed_enrollments,
)
assert returned_from_enrollment == patched_deactivate_enrollments.return_value
assert returned_to_enrollment == patched_create_enrollments.return_value[0][0]
patched_create_enrollments.assert_called_once_with(
existing_enrollment.user,
[target_run],
keep_failed_enrollments=keep_failed_enrollments,
mode=EDX_ENROLLMENT_VERIFIED_MODE,
)
patched_deactivate_enrollments.assert_called_once_with(
existing_enrollment,
ENROLL_CHANGE_STATUS_DEFERRED,
keep_failed_enrollments=keep_failed_enrollments,
)
else:
with pytest.raises(Exception):
defer_enrollment(

new_enrollment = CourseRunEnrollmentFactory.create(run=course_runs[1])
return_values = [
([new_enrollment] if edx_enroll_succeeds else [], edx_enroll_succeeds),
(
[existing_enrollment] if edx_downgrade_succeeds else [],
edx_downgrade_succeeds,
),
]

with patch(
"courses.api.create_run_enrollments", autospec=True
) as patched_create_enrollments:
patched_create_enrollments.side_effect = return_values

if keep_failed_enrollments or (edx_enroll_succeeds and edx_downgrade_succeeds):
returned_from_enrollment, returned_to_enrollment = defer_enrollment(
existing_enrollment.user,
existing_enrollment.run.courseware_id,
course_runs[1].courseware_id,
keep_failed_enrollments=keep_failed_enrollments,
)
assert patched_create_enrollments.call_count == 2
assert returned_from_enrollment == (
existing_enrollment if edx_downgrade_succeeds else None
)
assert returned_to_enrollment == (
new_enrollment if edx_enroll_succeeds else None
)
patched_create_enrollments.assert_has_calls(
[
call(
user=existing_enrollment.user,
runs=[course_runs[1]],
change_status=None,
keep_failed_enrollments=keep_failed_enrollments,
mode=EDX_ENROLLMENT_VERIFIED_MODE,
),
call(
user=existing_enrollment.user,
runs=[existing_enrollment.run],
change_status=ENROLL_CHANGE_STATUS_DEFERRED,
keep_failed_enrollments=keep_failed_enrollments,
mode=EDX_ENROLLMENT_AUDIT_MODE,
),
]
)
paid_course_run.refresh_from_db()
assert paid_course_run.course_run == course_runs[1]
else:
with pytest.raises(Exception):
defer_enrollment(
existing_enrollment.user,
existing_enrollment.run.courseware_id,
course_runs[1].courseware_id,
keep_failed_enrollments=keep_failed_enrollments,
)


def test_defer_enrollment_validation(mocker, user):
Expand Down Expand Up @@ -886,16 +903,36 @@ def test_defer_enrollment_validation(mocker, user):
user,
enrollments[0].run.courseware_id,
enrollments[1].run.courseware_id,
keep_failed_enrollments=True,
force=True,
)
assert patched_create_enrollments.call_count == 1
assert patched_create_enrollments.call_count == 2
patched_create_enrollments.assert_has_calls(
[
call(
user=user,
runs=[enrollments[1].run],
change_status=None,
keep_failed_enrollments=True,
mode=EDX_ENROLLMENT_VERIFIED_MODE,
),
call(
user=user,
runs=[enrollments[0].run],
change_status=ENROLL_CHANGE_STATUS_DEFERRED,
keep_failed_enrollments=True,
mode=EDX_ENROLLMENT_AUDIT_MODE,
),
]
)

defer_enrollment(
user,
enrollments[1].run.courseware_id,
enrollments[2].run.courseware_id,
force=True,
)
assert patched_create_enrollments.call_count == 2
assert patched_create_enrollments.call_count == 4


@pytest.mark.parametrize(
Expand Down
17 changes: 17 additions & 0 deletions courses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,23 @@ def deactivate_and_save(self, change_status, no_user=False):

return super().deactivate_and_save(change_status, no_user)

def change_payment_to_run(self, to_run):
"""
During a deferral process, if user has paid for this run
we can change the payment to another run
"""
# Due to circular dependancy importing locally
from ecommerce.models import Order

paid_run = PaidCourseRun.objects.filter(
user=self.user,
course_run=self.run,
order__state=Order.STATE.FULFILLED,
).first()
if paid_run:
paid_run.course_run = to_run
paid_run.save()

def to_dict(self):
return {**super().to_dict(), "text_id": self.run.courseware_id}

Expand Down
23 changes: 22 additions & 1 deletion courses/models_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@
ProgramRequirement,
ProgramRequirementNodeType,
limit_to_certificate_pages,
PaidCourseRun,
)
from ecommerce.factories import ProductFactory
from ecommerce.factories import ProductFactory, OrderFactory
from ecommerce.models import Order
from main.test_utils import format_as_iso8601
from users.factories import UserFactory

Expand Down Expand Up @@ -351,6 +353,25 @@ def test_deactivate_and_save():
enrollment.change_status = ENROLL_CHANGE_STATUS_REFUNDED


def test_change_payment_to_run():
"""Test that the change_payment_to_run updates the obj to new run"""
user = UserFactory.create()
course_run_enrollment = CourseRunEnrollmentFactory.create(
user=user, active=True, change_status=None
)

fulfilled_order = OrderFactory.create(purchaser=user, state=Order.STATE.FULFILLED)
paid_course_run = PaidCourseRun.objects.create(
user=user,
course_run=course_run_enrollment.run,
order=fulfilled_order,
)
new_run = CourseRunFactory.create()
course_run_enrollment.change_payment_to_run(new_run)
paid_course_run.refresh_from_db()
assert paid_course_run.course_run == new_run


@pytest.mark.parametrize(
"readable_id_value",
["somevalue", "some-value", "some_value", "some+value", "some:value"],
Expand Down
1 change: 1 addition & 0 deletions sheets/deferrals_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def deferrals_process_request(
user,
from_courseware_id,
to_courseware_id,
keep_failed_enrollments=True,
force=True,
)
if to_courseware_id and not to_enrollment:
Expand Down

0 comments on commit a8bb61d

Please sign in to comment.