feat: add course unenrollment filter before unenrollment starts

This commit is contained in:
Maria Grimaldi
2022-02-16 14:25:53 -04:00
parent e9e74f941f
commit 73533f021e
4 changed files with 161 additions and 6 deletions

View File

@@ -66,7 +66,7 @@ from openedx_events.learning.signals import (
COURSE_ENROLLMENT_CREATED,
COURSE_UNENROLLMENT_COMPLETED,
)
from openedx_filters.learning.filters import CourseEnrollmentStarted
from openedx_filters.learning.filters import CourseEnrollmentStarted, CourseUnenrollmentStarted
import openedx.core.djangoapps.django_comment_common.comment_client as cc
from common.djangoapps.course_modes.models import CourseMode, get_cosmetic_verified_display_price
from common.djangoapps.student.emails import send_proctoring_requirements_email
@@ -1122,6 +1122,10 @@ class EnrollmentNotAllowed(CourseEnrollmentException):
pass
class UnenrollmentNotAllowed(CourseEnrollmentException):
pass
class CourseEnrollmentManager(models.Manager):
"""
Custom manager for CourseEnrollment with Table-level filter methods.
@@ -1767,6 +1771,14 @@ class CourseEnrollment(models.Model):
try:
record = cls.objects.get(user=user, course_id=course_id)
try:
# .. filter_implemented_name: CourseUnenrollmentStarted
# .. filter_type: org.openedx.learning.course.unenrollment.started.v1
record = CourseUnenrollmentStarted.run_filter(enrollment=record)
except CourseUnenrollmentStarted.PreventUnenrollment as exc:
raise UnenrollmentNotAllowed(str(exc)) from exc
record.update_enrollment(is_active=False, skip_refund=skip_refund)
except cls.DoesNotExist:

View File

@@ -1,13 +1,15 @@
"""
Test that various filters are fired for models in the student app.
Test that various filters are fired for models/views in the student app.
"""
from django.test import override_settings
from django.urls import reverse
from openedx_filters import PipelineStep
from openedx_filters.learning.filters import CourseEnrollmentStarted, CourseUnenrollmentStarted
from rest_framework import status
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from openedx_filters.learning.filters import CourseEnrollmentStarted
from openedx_filters import PipelineStep
from common.djangoapps.student.models import CourseEnrollment, EnrollmentNotAllowed
from common.djangoapps.student.models import CourseEnrollment, EnrollmentNotAllowed, UnenrollmentNotAllowed
from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
@@ -24,6 +26,23 @@ class TestEnrollmentPipelineStep(PipelineStep):
return {"mode": "honor"}
class TestUnenrollmentPipelineStep(PipelineStep):
"""
Utility function used when getting steps for pipeline.
"""
def run_filter(self, enrollment): # pylint: disable=arguments-differ
"""Pipeline steps that modifies user's profile before unenrolling."""
if enrollment.mode == "no-id-professional":
raise CourseUnenrollmentStarted.PreventUnenrollment(
"You can't un-enroll from this site."
)
enrollment.user.profile.set_meta({"unenrolled_from": str(enrollment.course_id)})
enrollment.user.profile.save()
return {}
@skip_unless_lms
class EnrollmentFiltersTest(ModuleStoreTestCase):
"""
@@ -102,3 +121,117 @@ class EnrollmentFiltersTest(ModuleStoreTestCase):
self.assertEqual('audit', enrollment.mode)
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
@skip_unless_lms
class UnenrollmentFiltersTest(ModuleStoreTestCase):
"""
Tests for the Open edX Filters associated with the unenrollment process through the unenroll method.
This class guarantees that the following filters are triggered during the user's unenrollment:
- CourseUnenrollmentStarted
"""
USERNAME = "test"
EMAIL = "test@example.com"
PASSWORD = "password"
def setUp(self): # pylint: disable=arguments-differ
super().setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.course.unenrollment.started.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestUnenrollmentPipelineStep",
],
"fail_silently": False,
},
},
)
def test_unenrollment_filter_executed(self):
"""
Test whether the student unenrollment filter is triggered before the user's
unenrollment process.
Expected result:
- CourseUnenrollmentStarted is triggered and executes TestUnenrollmentPipelineStep.
- The user's profile has unenrolled_from in its meta field.
"""
CourseEnrollment.enroll(self.user, self.course.id, mode="audit")
CourseEnrollment.unenroll(self.user, self.course.id)
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.course.unenrollment.started.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestUnenrollmentPipelineStep",
],
"fail_silently": False,
},
},
)
def test_unenrollment_filter_prevent_unenroll(self):
"""
Test prevent the user's unenrollment through a pipeline step.
Expected result:
- CourseUnenrollmentStarted is triggered and executes TestUnenrollmentPipelineStep.
- The user can't unenroll.
"""
CourseEnrollment.enroll(self.user, self.course.id, mode="no-id-professional")
with self.assertRaises(UnenrollmentNotAllowed):
CourseEnrollment.unenroll(self.user, self.course.id)
@override_settings(OPEN_EDX_FILTERS_CONFIG={})
def test_unenrollment_without_filter_configuration(self):
"""
Test usual unenrollment process without filter's intervention.
Expected result:
- CourseUnenrollmentStarted does not have any effect on the unenrollment process.
- The unenrollment process ends successfully.
"""
CourseEnrollment.enroll(self.user, self.course.id, mode="audit")
CourseEnrollment.unenroll(self.user, self.course.id)
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.course.unenrollment.started.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestUnenrollmentPipelineStep",
],
"fail_silently": False,
},
},
)
def test_unenrollment_blocked_by_filter(self):
"""
Test cannot unenroll using change_enrollment view course when UnenrollmentNotAllowed is
raised by unenroll method.
Expected result:
- CourseUnenrollmentStarted does not have any effect on the unenrollment process.
- The unenrollment process ends successfully.
"""
CourseEnrollment.enroll(self.user, self.course.id, mode="no-id-professional")
params = {
"enrollment_action": "unenroll",
"course_id": str(self.course.id)
}
response = self.client.post(reverse("change_enrollment"), params)
self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
self.assertEqual("You can't un-enroll from this site.", response.content.decode("utf-8"))

View File

@@ -69,6 +69,7 @@ from common.djangoapps.student.models import ( # lint-amnesty, pylint: disable=
PendingSecondaryEmailChange,
Registration,
RegistrationCookieConfiguration,
UnenrollmentNotAllowed,
UserAttribute,
UserProfile,
UserSignupSource,
@@ -410,7 +411,11 @@ def change_enrollment(request, check_access=True):
if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES:
return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course"))
CourseEnrollment.unenroll(user, course_id)
try:
CourseEnrollment.unenroll(user, course_id)
except UnenrollmentNotAllowed as exc:
return HttpResponseBadRequest(str(exc))
REFUND_ORDER.send(sender=None, course_enrollment=enrollment)
return HttpResponse()
else:

View File

@@ -75,6 +75,11 @@ class UnenrollView extends Backbone.View {
this.switchToSlideOne();
this.$('.reasons_survey:first .submit_reasons').click(this.switchToSlideTwo.bind(this));
}
} else if (xhr.status === 400) {
$('#unenroll_error').text(
xhr.responseText,
).stop()
.css('display', 'block');
} else if (xhr.status === 403) {
location.href = `${this.urls.signInUser}?course_id=${
encodeURIComponent($('#unenroll_course_id').val())}&enrollment_action=unenroll`;