Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
COURSE_ENROLLMENT_CREATED,
COURSE_UNENROLLMENT_COMPLETED,
)
from openedx_filters.learning.enrollment import PreEnrollmentFilter
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
Expand Down Expand Up @@ -1113,6 +1114,10 @@ class AlreadyEnrolledError(CourseEnrollmentException):
pass


class EnrollmentNotAllowed(CourseEnrollmentException):
pass


class CourseEnrollmentManager(models.Manager):
"""
Custom manager for CourseEnrollment with Table-level filter methods.
Expand Down Expand Up @@ -1614,6 +1619,13 @@ def enroll(cls, user, course_key, mode=None, check_access=False, can_upgrade=Fal

Also emits relevant events for analytics purposes.
"""
try:
user, course_key, mode = PreEnrollmentFilter.run(
user=user, course_key=course_key, mode=mode,
)
except PreEnrollmentFilter.PreventEnrollment as exc:
Comment thread
felipemontoya marked this conversation as resolved.
raise EnrollmentNotAllowed(str(exc)) from exc

if mode is None:
mode = _default_course_mode(str(course_key))
# All the server-side checks for whether a user is allowed to enroll.
Expand Down
93 changes: 93 additions & 0 deletions common/djangoapps/student/tests/test_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
Test that various filters are fired for models in the student app.
"""
from common.djangoapps.student.models import CourseEnrollment, EnrollmentNotAllowed
from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory

from openedx.core.djangolib.testing.utils import skip_unless_lms
from django.test import TestCase, override_settings
from openedx_filters.learning.enrollment import PreEnrollmentFilter


from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory

from openedx_filters import PipelineStep


class TestEnrollmentPipelineStep(PipelineStep):
"""
Utility function used when getting steps for pipeline.
"""

def run(self, user, course_key, mode):
"""Pipeline steps that changes mode to honor."""
if mode == "no-id-professional":
raise PreEnrollmentFilter.PreventEnrollment()
return {"mode": "honor"}


@skip_unless_lms
class EnrollmentFiltersTest(ModuleStoreTestCase):
"""
Tests for the Open edX Filters associated with the enrollment process through the enroll method.

This class guarantees that the following filters are triggered during the user's enrollment:

- PreEnrollmentFilter
"""

def setUp(self): # pylint: disable=arguments-differ
super().setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create(
username="test",
email="test@example.com",
password="password",
)
self.user_profile = UserProfileFactory.create(user=self.user, name="Test Example")

@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.course.enrollment.started.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestEnrollmentPipelineStep",
],
"fail_silently": False,
},
},
)
def test_enrollment_filter_executed(self):
"""
Test whether the student enrollment filter is triggered before the user's
enrollment process.

Expected result:
- PreEnrollmentFilter is triggered and executes TestEnrollmentPipelineStep.
- The arguments that the receiver gets are the arguments used by the filter
with the enrollment mode changed.
"""
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='audit')

self.assertEqual('honor', enrollment.mode)

@override_settings(
OPEN_EDX_FILTERS_CONFIG={
"org.openedx.learning.course.enrollment.started.v1": {
"pipeline": [
"common.djangoapps.student.tests.test_filters.TestEnrollmentPipelineStep",
],
"fail_silently": False,
},
},
)
def test_enrollment_filter_prevent_enroll(self):
"""
Test prevent the user's enrollment through a pipeline step.

Expected result:
- PreEnrollmentFilter is triggered and executes TestEnrollmentPipelineStep.
- The user can't enroll.
"""
with self.assertRaises(EnrollmentNotAllowed):
CourseEnrollment.enroll(self.user, self.course.id, mode='no-id-professional')
6 changes: 5 additions & 1 deletion openedx/core/djangoapps/enrollments/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
CourseEnrollmentClosedError,
CourseEnrollmentExistsError,
CourseEnrollmentFullError,
CourseEnrollmentNotAllowedError,
InvalidEnrollmentAttribute,
UserNotFoundError
)
Expand All @@ -28,7 +29,8 @@
CourseEnrollmentAttribute,
CourseFullError,
EnrollmentClosedError,
NonExistentCourseError
NonExistentCourseError,
EnrollmentNotAllowed
)
from common.djangoapps.student.roles import RoleCache

Expand Down Expand Up @@ -162,6 +164,8 @@ def create_course_enrollment(username, course_id, mode, is_active):
except AlreadyEnrolledError as err:
enrollment = get_course_enrollment(username, course_id)
raise CourseEnrollmentExistsError(str(err), enrollment) # lint-amnesty, pylint: disable=raise-missing-from
except EnrollmentNotAllowed as err:
raise CourseEnrollmentNotAllowedError(str(err)) # lint-amnesty, pylint: disable=raise-missing-from


def update_course_enrollment(username, course_id, mode=None, is_active=None):
Expand Down
4 changes: 4 additions & 0 deletions openedx/core/djangoapps/enrollments/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class CourseEnrollmentFullError(CourseEnrollmentError):
pass


class CourseEnrollmentNotAllowedError(CourseEnrollmentError):
pass


class CourseEnrollmentExistsError(CourseEnrollmentError): # lint-amnesty, pylint: disable=missing-class-docstring
enrollment = None

Expand Down
1 change: 1 addition & 0 deletions requirements/edunext/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ eox-theming # Edunext theming plugin. This plugin allows
# Libraries #
###################
openedx-events # Open edX Events from Hooks Extension Framework (OEP-50)
openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50)

#####################
# eduNEXT Xblocks #
Expand Down
7 changes: 4 additions & 3 deletions requirements/edunext/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ django-oauth-toolkit==1.3.2 # via -c requirements/edunext/../edx/base.txt, eox-
django-oauth2-provider==0.2.6.1 # via eox-core
django-waffle==2.1.0 # via -c requirements/edunext/../edx/base.txt, edx-django-utils, edx-drf-extensions, edx-proctoring, eox-core
django-webpack-loader==0.7.0 # via -c requirements/edunext/../edx/base.txt, edx-proctoring
django==2.2.20 # via -c requirements/edunext/../edx/base.txt, django-crum, django-filter, django-model-utils, django-oauth-toolkit, djangorestframework, drf-jwt, drf-yasg, edx-api-doc-tools, edx-django-utils, edx-drf-extensions, edx-opaque-keys, edx-proctoring, edx-when, eox-audit-model, eox-hooks, event-tracking, jsonfield2, openedx-events, rest-condition
django==2.2.20 # via -c requirements/edunext/../edx/base.txt, django-crum, django-filter, django-model-utils, django-oauth-toolkit, djangorestframework, drf-jwt, drf-yasg, edx-api-doc-tools, edx-django-utils, edx-drf-extensions, edx-opaque-keys, edx-proctoring, edx-when, eox-audit-model, eox-hooks, event-tracking, jsonfield2, openedx-events, openedx-filters, rest-condition
djangorestframework==3.12.4 # via -c requirements/edunext/../edx/base.txt, drf-jwt, drf-yasg, edx-api-doc-tools, edx-drf-extensions, edx-proctoring, eox-core, eox-hooks, rest-condition
drf-jwt==1.19.0 # via -c requirements/edunext/../edx/base.txt, edx-drf-extensions
drf-yasg==1.20.0 # via -c requirements/edunext/../edx/base.txt, edx-api-doc-tools
Expand All @@ -40,7 +40,7 @@ eox-audit-model==0.6.0 # via eox-core, eox-tagging
eox-core[eox-audit,sentry,tpa]==5.0.3 # via -r requirements/edunext/base.in, eox-tagging
eox-hooks==2.0.0 # via -r requirements/edunext/base.in
eox-tagging==3.0.0 # via -r requirements/edunext/base.in
eox-tenant==5.0.1 # via -r requirements/edunext/base.in
eox-tenant==5.1.0 # via -r requirements/edunext/base.in
eox-theming==2.0.0 # via -r requirements/edunext/base.in
event-tracking==1.0.4 # via -c requirements/edunext/../edx/base.txt, edx-proctoring
fs==2.0.18 # via -c requirements/edunext/../edx/base.txt, xblock
Expand All @@ -56,6 +56,7 @@ markupsafe==1.1.1 # via -c requirements/edunext/../edx/base.txt, jinja2,
newrelic==6.2.0.156 # via -c requirements/edunext/../edx/base.txt, edx-django-utils
oauthlib==3.0.1 # via -c requirements/edunext/../edx/base.txt, django-oauth-toolkit, requests-oauthlib, social-auth-core
openedx-events==0.6.0 # via -r requirements/edunext/base.in, eox-hooks
openedx-filters==0.3.0 # via -r requirements/edunext/base.in
packaging==20.9 # via -c requirements/edunext/../edx/base.txt, drf-yasg
pbr==5.5.1 # via -c requirements/edunext/../edx/base.txt, stevedore
psutil==5.8.0 # via -c requirements/edunext/../edx/base.txt, edx-django-utils
Expand All @@ -74,7 +75,7 @@ pyyaml==5.4.1 # via -c requirements/edunext/../edx/base.txt, xblock
requests-oauthlib==1.3.0 # via -c requirements/edunext/../edx/base.txt, social-auth-core
requests==2.25.1 # via -c requirements/edunext/../edx/base.txt, coreapi, django-oauth-toolkit, edx-drf-extensions, edx-rest-api-client, pyjwkest, requests-oauthlib, slumber, social-auth-core
rest-condition==1.0.3 # via -c requirements/edunext/../edx/base.txt, edx-drf-extensions
rsa==4.7.2 # via python-jose
rsa==4.8 # via python-jose
ruamel.yaml.clib==0.2.2 # via -c requirements/edunext/../edx/base.txt, ruamel.yaml
ruamel.yaml==0.17.4 # via -c requirements/edunext/../edx/base.txt, drf-yasg
rules==2.2 # via -c requirements/edunext/../edx/base.txt, edx-proctoring
Expand Down