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
6 changes: 3 additions & 3 deletions common/djangoapps/student/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
COURSE_ENROLLMENT_CREATED,
COURSE_UNENROLLMENT_COMPLETED,
)
from openedx_filters.learning.enrollment import PreEnrollmentFilter
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 @@ -1620,10 +1620,10 @@ 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, course_key, mode = CourseEnrollmentStarted.run_filter(
user=user, course_key=course_key, mode=mode,
)
except PreEnrollmentFilter.PreventEnrollment as exc:
except CourseEnrollmentStarted.PreventEnrollment as exc:
raise EnrollmentNotAllowed(str(exc)) from exc

if mode is None:
Expand Down
31 changes: 22 additions & 9 deletions common/djangoapps/student/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,26 @@
Test that various filters are fired for models in the student app.
"""
from django.test import override_settings
from openedx_filters.learning.enrollment import PreEnrollmentFilter
from openedx_filters import PipelineStep
from openedx_filters.learning.filters import CourseEnrollmentStarted
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory

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 xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory


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

def run(self, user, course_key, mode): # pylint: disable=unused-argument, arguments-differ
def run_filter(self, user, course_key, mode): # pylint: disable=arguments-differ, unused-argument
"""Pipeline steps that changes mode to honor."""
if mode == "no-id-professional":
raise PreEnrollmentFilter.PreventEnrollment()
raise CourseEnrollmentStarted.PreventEnrollment()
return {"mode": "honor"}


Expand All @@ -33,7 +32,7 @@ class EnrollmentFiltersTest(ModuleStoreTestCase):

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

- PreEnrollmentFilter
- CourseEnrollmentStarted
"""

def setUp(self): # pylint: disable=arguments-differ
Expand Down Expand Up @@ -62,7 +61,7 @@ def test_enrollment_filter_executed(self):
enrollment process.

Expected result:
- PreEnrollmentFilter is triggered and executes TestEnrollmentPipelineStep.
- CourseEnrollmentStarted is triggered and executes TestEnrollmentPipelineStep.
- The arguments that the receiver gets are the arguments used by the filter
with the enrollment mode changed.
"""
Expand All @@ -85,8 +84,22 @@ 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.
- 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))
96 changes: 94 additions & 2 deletions common/djangoapps/student/views/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from django.db import transaction
from django.db.models.signals import post_save
from django.dispatch import Signal, receiver # lint-amnesty, pylint: disable=unused-import
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect

from django.shortcuts import redirect
from django.template.context_processors import csrf
from django.urls import reverse
Expand All @@ -32,6 +33,8 @@
# Note that this lives in LMS, so this dependency should be refactored.
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from openedx_filters.exceptions import OpenEdxFilterException
from openedx_filters.tooling import OpenEdxPublicFilter
from pytz import UTC

from common.djangoapps.track import views as track_views
Expand Down Expand Up @@ -105,6 +108,79 @@ def csrf_token(context):
' name="csrfmiddlewaretoken" value="{}" /></div>').format(Text(token)))


class HomepageRenderStarted(OpenEdxPublicFilter):
"""
Custom class used to create homepage render filters and its custom methods.
"""

filter_type = "org.openedx.learning.homepage.render.started.v1"

class RedirectToPage(OpenEdxFilterException):
"""
Custom class used to stop the homepage rendering process.
"""

def __init__(self, message, redirect_to=""):
"""
Override init that defines specific arguments used in the homepage render process.

Arguments:
message: error message for the exception.
redirect_to: URL to redirect to.
"""
super().__init__(message, redirect_to=redirect_to)

class RenderInvalidHomepage(OpenEdxFilterException):
"""
Custom class used to stop the homepage render process.
"""

def __init__(self, message, index_template="", template_context=None):
"""
Override init that defines specific arguments used in the index render process.

Arguments:
message: error message for the exception.
index_template: template path rendered instead.
template_context: context used to the new index_template.
"""
super().__init__(
message,
index_template=index_template,
template_context=template_context,
)

class RenderCustomResponse(OpenEdxFilterException):
"""
Custom class used to stop the homepage rendering process.
"""

def __init__(self, message, response=None):
"""
Override init that defines specific arguments used in the homepage render process.

Arguments:
message: error message for the exception.
response: custom response which will be returned by the homepage view.
"""
super().__init__(
message,
response=response,
)

@classmethod
def run_filter(cls, context, template_name):
"""
Execute a filter with the signature specified.

Arguments:
context (dict): context dictionary for homepage template.
template_name (str): template name to be rendered by the homepage.
"""
data = super().run_pipeline(context=context, template_name=template_name)
return data.get("context"), data.get("template_name")


# NOTE: This view is not linked to directly--it is called from
# branding/views.py:index(), which is cached for anonymous users.
# This means that it should always return the same thing for anon
Expand Down Expand Up @@ -159,7 +235,23 @@ def index(request, extra_context=None, user=AnonymousUser()):
# Add marketable programs to the context.
context['programs_list'] = get_programs_with_type(request.site, include_hidden=False)

return render_to_response('index.html', context)
index_template = 'index.html'
try:
# .. filter_implemented_name: HomepageRenderStarted
# .. filter_type: org.openedx.learning.homepage.render.started.v1
context, index_template = HomepageRenderStarted.run_filter(
context=context, template_name=index_template,
)
except HomepageRenderStarted.RenderInvalidHomepage as exc:
response = render_to_response(exc.index_template, exc.template_context) # pylint: disable=no-member
except HomepageRenderStarted.RedirectToPage as exc:
response = HttpResponseRedirect(exc.redirect_to or reverse('account_settings'))
except HomepageRenderStarted.RenderCustomResponse as exc:
response = exc.response # pylint: disable=no-member
else:
response = render_to_response(index_template, context)

return response


def compose_activation_email(root_url, user, user_registration=None, route_enabled=False, profile_name=''):
Expand Down
107 changes: 98 additions & 9 deletions lms/djangoapps/courseware/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Q, prefetch_related_objects
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import redirect
from django.template.context_processors import csrf
from django.urls import reverse
Expand All @@ -38,6 +38,8 @@
from markupsafe import escape
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx_filters.exceptions import OpenEdxFilterException
from openedx_filters.tooling import OpenEdxPublicFilter
from pytz import UTC
from requests.exceptions import ConnectionError, Timeout # pylint: disable=redefined-builtin
from rest_framework import status
Expand Down Expand Up @@ -252,6 +254,79 @@ def user_groups(user):
return group_names


class CatalogRenderStarted(OpenEdxPublicFilter):
"""
Custom class used to create catalog render filters and its custom methods.
"""

filter_type = "org.openedx.learning.catalog.render.started.v1"

class RedirectToPage(OpenEdxFilterException):
"""
Custom class used to stop the catalog rendering process.
"""

def __init__(self, message, redirect_to=""):
"""
Override init that defines specific arguments used in the catalog render process.

Arguments:
message: error message for the exception.
redirect_to: URL to redirect to.
"""
super().__init__(message, redirect_to=redirect_to)

class RenderInvalidCatalog(OpenEdxFilterException):
"""
Custom class used to stop the catalog render process.
"""

def __init__(self, message, courses_template="", template_context=None):
"""
Override init that defines specific arguments used in the index render process.

Arguments:
message: error message for the exception.
courses_template: template path rendered instead.
template_context: context used to the new courses_template.
"""
super().__init__(
message,
courses_template=courses_template,
template_context=template_context,
)

class RenderCustomResponse(OpenEdxFilterException):
"""
Custom class used to stop the catalog rendering process.
"""

def __init__(self, message, response=None):
"""
Override init that defines specific arguments used in the catalog render process.

Arguments:
message: error message for the exception.
response: custom response which will be returned by the catalog view.
"""
super().__init__(
message,
response=response,
)

@classmethod
def run_filter(cls, context, template_name):
"""
Execute a filter with the signature specified.

Arguments:
context (dict): context dictionary for catalog template.
template_name (str): template name to be rendered by the catalog.
"""
data = super().run_pipeline(context=context, template_name=template_name)
return data.get("context"), data.get("template_name")


@ensure_csrf_cookie
@cache_if_anonymous()
def courses(request):
Expand All @@ -272,14 +347,28 @@ def courses(request):
# Add marketable programs to the context.
programs_list = get_programs_with_type(request.site, include_hidden=False)

return render_to_response(
"courseware/courses.html",
{
'courses': courses_list,
'course_discovery_meanings': course_discovery_meanings,
'programs_list': programs_list,
}
)
courses_template = "courseware/courses.html"
context = {
'courses': courses_list,
'course_discovery_meanings': course_discovery_meanings,
'programs_list': programs_list,
}
try:
# .. filter_implemented_name: CatalogRenderStarted
# .. filter_type: org.openedx.learning.catalog.render.started.v1
context, courses_template = CatalogRenderStarted.run_filter(
context=context, template_name=courses_template,
)
except CatalogRenderStarted.RenderInvalidCatalog as exc:
response = render_to_response(exc.courses_template, exc.template_context) # pylint: disable=no-member
except CatalogRenderStarted.RedirectToPage as exc:
response = HttpResponseRedirect(exc.redirect_to or reverse('account_settings'))
except CatalogRenderStarted.RenderCustomResponse as exc:
response = exc.response # pylint: disable=no-member
else:
response = render_to_response(courses_template, context)

return response


class PerUserVideoMetadataThrottle(UserRateThrottle):
Expand Down
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 @@ -30,6 +30,8 @@

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.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 @@ -515,6 +517,16 @@ 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, # pylint: disable=no-member
error_code=exc.error_code, # pylint: disable=no-member
context=exc.context, # pylint: disable=no-member
) 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
Loading