Skip to content

Commit d0ac319

Browse files
mariajgrimaldiMaferMazu
authored andcommitted
feat: add first batch of Open edX Filters
* Add PreEnrollmentFilter * Add PreRegisterFilter * Add PreLoginFilter For more info: openedx/edx-platform#29449 Some events that were already on the platform were also added: * Add COURSE_ENROLLMENT_CHANGED: sent after the enrollment update * Add COURSE_ENROLLMENT_CREATED event after the user's enrollment creation * Add COURSE_UNENROLLMENT_COMPLETED: sent after the user's unenrollment For more info: openedx/edx-platform#28266 openedx/edx-platform#28640
1 parent 8d94dcf commit d0ac319

File tree

9 files changed

+432
-0
lines changed

9 files changed

+432
-0
lines changed

common/djangoapps/student/models.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@
5555
from slumber.exceptions import HttpClientError, HttpServerError
5656
from user_util import user_util
5757

58+
from openedx_events.learning.data import (
59+
CourseData,
60+
CourseEnrollmentData,
61+
UserData,
62+
UserPersonalData,
63+
)
64+
from openedx_events.learning.signals import (
65+
COURSE_ENROLLMENT_CHANGED,
66+
COURSE_ENROLLMENT_CREATED,
67+
COURSE_UNENROLLMENT_COMPLETED,
68+
)
69+
from openedx_filters.learning.filters import CourseEnrollmentStarted
5870
import openedx.core.djangoapps.django_comment_common.comment_client as cc
5971
from common.djangoapps.course_modes.models import CourseMode, get_cosmetic_verified_display_price
6072
from common.djangoapps.student.emails import send_proctoring_requirements_email
@@ -1100,6 +1112,10 @@ class AlreadyEnrolledError(CourseEnrollmentException):
11001112
pass
11011113

11021114

1115+
class EnrollmentNotAllowed(CourseEnrollmentException):
1116+
pass
1117+
1118+
11031119
class CourseEnrollmentManager(models.Manager):
11041120
"""
11051121
Custom manager for CourseEnrollment with Table-level filter methods.
@@ -1555,6 +1571,13 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal
15551571
15561572
Also emits relevant events for analytics purposes.
15571573
"""
1574+
try:
1575+
user, course_key, mode = CourseEnrollmentStarted.run_filter(
1576+
user=user, course_key=course_key, mode=mode,
1577+
)
1578+
except CourseEnrollmentStarted.PreventEnrollment as exc:
1579+
raise EnrollmentNotAllowed(str(exc)) from exc
1580+
15581581
if mode is None:
15591582
mode = _default_course_mode(str(course_key))
15601583
# All the server-side checks for whether a user is allowed to enroll.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
Test that various filters are fired for models in the student app.
3+
"""
4+
from django.test import override_settings
5+
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
6+
from xmodule.modulestore.tests.factories import CourseFactory
7+
from openedx_filters.learning.filters import CourseEnrollmentStarted
8+
from openedx_filters import PipelineStep
9+
10+
from common.djangoapps.student.models import CourseEnrollment, EnrollmentNotAllowed
11+
from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory
12+
from openedx.core.djangolib.testing.utils import skip_unless_lms
13+
14+
15+
class TestEnrollmentPipelineStep(PipelineStep):
16+
"""
17+
Utility function used when getting steps for pipeline.
18+
"""
19+
20+
def run_filter(self, user, course_key, mode): # pylint: disable=arguments-differ
21+
"""Pipeline steps that changes mode to honor."""
22+
if mode == "no-id-professional":
23+
raise CourseEnrollmentStarted.PreventEnrollment()
24+
return {"mode": "honor"}
25+
26+
27+
@skip_unless_lms
28+
class EnrollmentFiltersTest(ModuleStoreTestCase):
29+
"""
30+
Tests for the Open edX Filters associated with the enrollment process through the enroll method.
31+
32+
This class guarantees that the following filters are triggered during the user's enrollment:
33+
34+
- CourseEnrollmentStarted
35+
"""
36+
37+
def setUp(self): # pylint: disable=arguments-differ
38+
super().setUp()
39+
self.course = CourseFactory.create()
40+
self.user = UserFactory.create(
41+
username="test",
42+
43+
password="password",
44+
)
45+
self.user_profile = UserProfileFactory.create(user=self.user, name="Test Example")
46+
47+
@override_settings(
48+
OPEN_EDX_FILTERS_CONFIG={
49+
"org.openedx.learning.course.enrollment.started.v1": {
50+
"pipeline": [
51+
"common.djangoapps.student.tests.test_filters.TestEnrollmentPipelineStep",
52+
],
53+
"fail_silently": False,
54+
},
55+
},
56+
)
57+
def test_enrollment_filter_executed(self):
58+
"""
59+
Test whether the student enrollment filter is triggered before the user's
60+
enrollment process.
61+
62+
Expected result:
63+
- CourseEnrollmentStarted is triggered and executes TestEnrollmentPipelineStep.
64+
- The arguments that the receiver gets are the arguments used by the filter
65+
with the enrollment mode changed.
66+
"""
67+
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='audit')
68+
69+
self.assertEqual('honor', enrollment.mode)
70+
71+
@override_settings(
72+
OPEN_EDX_FILTERS_CONFIG={
73+
"org.openedx.learning.course.enrollment.started.v1": {
74+
"pipeline": [
75+
"common.djangoapps.student.tests.test_filters.TestEnrollmentPipelineStep",
76+
],
77+
"fail_silently": False,
78+
},
79+
},
80+
)
81+
def test_enrollment_filter_prevent_enroll(self):
82+
"""
83+
Test prevent the user's enrollment through a pipeline step.
84+
85+
Expected result:
86+
- CourseEnrollmentStarted is triggered and executes TestEnrollmentPipelineStep.
87+
- The user can't enroll.
88+
"""
89+
with self.assertRaises(EnrollmentNotAllowed):
90+
CourseEnrollment.enroll(self.user, self.course.id, mode='no-id-professional')
91+
92+
@override_settings(OPEN_EDX_FILTERS_CONFIG={})
93+
def test_enrollment_without_filter_configuration(self):
94+
"""
95+
Test usual enrollment process, without filter's intervention.
96+
97+
Expected result:
98+
- CourseEnrollmentStarted does not have any effect on the enrollment process.
99+
- The enrollment process ends successfully.
100+
"""
101+
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='audit')
102+
103+
self.assertEqual('audit', enrollment.mode)
104+
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))

openedx/core/djangoapps/user_authn/views/login.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
from ratelimit.decorators import ratelimit
2929
from rest_framework.views import APIView
3030

31+
from openedx_events.learning.data import UserData, UserPersonalData
32+
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
33+
from openedx_filters.learning.filters import StudentLoginRequested
34+
35+
from common.djangoapps import third_party_auth
3136
from common.djangoapps.edxmako.shortcuts import render_to_response
3237
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
3338
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
@@ -500,6 +505,13 @@ def login_user(request):
500505

501506
possibly_authenticated_user = user
502507

508+
try:
509+
possibly_authenticated_user = StudentLoginRequested.run_filter(user=possibly_authenticated_user)
510+
except StudentLoginRequested.PreventLogin as exc:
511+
raise AuthFailedError(
512+
str(exc), redirect_url=exc.redirect_to, error_code=exc.error_code, context=exc.context,
513+
) from exc
514+
503515
if not is_user_third_party_authenticated:
504516
possibly_authenticated_user = _authenticate_first_party(request, user, third_party_auth_requested)
505517
if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login():

openedx/core/djangoapps/user_authn/views/register.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
from django.views.decorators.debug import sensitive_post_parameters
2424
from edx_django_utils.monitoring import set_custom_attribute
2525
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
26+
from openedx_events.learning.data import UserData, UserPersonalData
27+
from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED
28+
from openedx_filters.learning.filters import StudentRegistrationRequested
2629
from pytz import UTC
2730
from ratelimit.decorators import ratelimit
2831
from requests import HTTPError
@@ -522,6 +525,14 @@ def post(self, request):
522525
data = request.POST.copy()
523526
self._handle_terms_of_service(data)
524527

528+
try:
529+
data = StudentRegistrationRequested.run_filter(form_data=data)
530+
except StudentRegistrationRequested.PreventRegistration as exc:
531+
errors = {
532+
"error_message": [{"user_message": str(exc)}],
533+
}
534+
return self._create_response(request, errors, status_code=exc.status_code)
535+
525536
response = self._handle_duplicate_email_username(request, data)
526537
if response:
527538
return response

0 commit comments

Comments
 (0)