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
23 changes: 23 additions & 0 deletions common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@
from slumber.exceptions import HttpClientError, HttpServerError
from user_util import user_util

from openedx_events.learning.data import (
CourseData,
CourseEnrollmentData,
UserData,
UserPersonalData,
)
from openedx_events.learning.signals import (
COURSE_ENROLLMENT_CHANGED,
COURSE_ENROLLMENT_CREATED,
COURSE_UNENROLLMENT_COMPLETED,
)
from openedx_filters.learning.filters import CourseEnrollmentStarted
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 @@ -1100,6 +1112,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 @@ -1555,6 +1571,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 = CourseEnrollmentStarted.run_filter(
user=user, course_key=course_key, mode=mode,
)
except CourseEnrollmentStarted.PreventEnrollment as exc:
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
104 changes: 104 additions & 0 deletions common/djangoapps/student/tests/test_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
Test that various filters are fired for models in the student app.
"""
from django.test import override_settings
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.tests.factories import UserFactory, UserProfileFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms


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

def run_filter(self, user, course_key, mode): # pylint: disable=arguments-differ
"""Pipeline steps that changes mode to honor."""
if mode == "no-id-professional":
raise CourseEnrollmentStarted.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:

- CourseEnrollmentStarted
"""

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:
- CourseEnrollmentStarted 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:
- CourseEnrollmentStarted 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')

@override_settings(OPEN_EDX_FILTERS_CONFIG={})
def test_enrollment_without_filter_configuration(self):
"""
Test usual enrollment process, without filter's intervention.

Expected result:
- CourseEnrollmentStarted does not have any effect on the enrollment process.
- The enrollment process ends successfully.
"""
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='audit')

self.assertEqual('audit', enrollment.mode)
self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
12 changes: 12 additions & 0 deletions openedx/core/djangoapps/user_authn/views/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
from ratelimit.decorators import ratelimit
from rest_framework.views import APIView

from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import SESSION_LOGIN_COMPLETED
from openedx_filters.learning.filters import StudentLoginRequested

from common.djangoapps import third_party_auth
from common.djangoapps.edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
Expand Down Expand Up @@ -500,6 +505,13 @@ def login_user(request):

possibly_authenticated_user = user

try:
possibly_authenticated_user = StudentLoginRequested.run_filter(user=possibly_authenticated_user)
except StudentLoginRequested.PreventLogin as exc:
raise AuthFailedError(
str(exc), redirect_url=exc.redirect_to, error_code=exc.error_code, context=exc.context,
) from exc

if not is_user_third_party_authenticated:
possibly_authenticated_user = _authenticate_first_party(request, user, third_party_auth_requested)
if possibly_authenticated_user and password_policy_compliance.should_enforce_compliance_on_login():
Expand Down
11 changes: 11 additions & 0 deletions openedx/core/djangoapps/user_authn/views/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
from django.views.decorators.debug import sensitive_post_parameters
from edx_django_utils.monitoring import set_custom_attribute
from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace
from openedx_events.learning.data import UserData, UserPersonalData
from openedx_events.learning.signals import STUDENT_REGISTRATION_COMPLETED
from openedx_filters.learning.filters import StudentRegistrationRequested
from pytz import UTC
from ratelimit.decorators import ratelimit
from requests import HTTPError
Expand Down Expand Up @@ -522,6 +525,14 @@ def post(self, request):
data = request.POST.copy()
self._handle_terms_of_service(data)

try:
data = StudentRegistrationRequested.run_filter(form_data=data)
except StudentRegistrationRequested.PreventRegistration as exc:
errors = {
"error_message": [{"user_message": str(exc)}],
}
return self._create_response(request, errors, status_code=exc.status_code)

response = self._handle_duplicate_email_username(request, data)
if response:
return response
Expand Down
Loading