feat: add course unenrollment filter before unenrollment starts
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user