diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index ed81d9101bcc..0f1d76170007 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -66,7 +66,7 @@ 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,12 @@ def unenroll(cls, user, course_id, skip_refund=False): try: record = cls.objects.get(user=user, course_id=course_id) + + try: + 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: diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index 56a77c57f12c..be57bf74e5ac 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -18,6 +18,7 @@ from edx_django_utils.plugins import get_plugins_view_context from edx_toggles.toggles import LegacyWaffleFlag, LegacyWaffleFlagNamespace from opaque_keys.edx.keys import CourseKey +from openedx_filters.learning.filters import DashboardRenderStarted from pytz import UTC from lms.djangoapps.bulk_email.api import is_bulk_email_feature_enabled @@ -65,6 +66,19 @@ experiments_namespace = LegacyWaffleFlagNamespace(name='student.experiments') +class DashboardException(Exception): + """ + Exception class that requires redirecting to a URL. + """ + def __init__(self, url): + super().__init__() + self.url = url + + +class DashboardRenderNotAllowed(DashboardException): + pass + + def get_org_black_and_whitelist_for_site(): """ Returns the org blacklist and whitelist for the current site. @@ -863,7 +877,15 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem 'resume_button_urls': resume_button_urls }) - response = render_to_response('dashboard.html', context) + dashboard_template = 'dashboard.html' + try: + context, dashboard_template = DashboardRenderStarted.run_filter( + context=context, template_name=dashboard_template, + ) + except DashboardRenderStarted.PreventDashboardRender as exc: + raise DashboardRenderNotAllowed(reverse(exc.redirect_to or 'account_settings')) from exc + + response = render_to_response(dashboard_template, context) if show_account_activation_popup: response.delete_cookie( settings.SHOW_ACTIVATE_CTA_POPUP_COOKIE_NAME, diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index ef2deca5e5a0..e234ab544b56 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -16,12 +16,12 @@ from django.db import models, transaction from django.db.models import Count from django.dispatch import receiver - from django.utils.translation import gettext_lazy as _ from edx_name_affirmation.api import get_verified_name, should_use_verified_name_for_certs from model_utils import Choices from model_utils.models import TimeStampedModel from opaque_keys.edx.django.models import CourseKeyField +from openedx_filters.learning.filters import CertificateCreationRequested from simple_history.models import HistoricalRecords from common.djangoapps.student import models_api as student_api @@ -50,6 +50,14 @@ class CertificateSocialNetworks: twitter = 'Twitter' +class GeneratedCertificateException(Exception): + pass + + +class CertificateGenerationNotAllowed(GeneratedCertificateException): + pass + + class CertificateAllowlist(TimeStampedModel): """ Tracks students who are on the certificate allowlist for a given course run. @@ -463,6 +471,13 @@ def save(self, *args, **kwargs): # pylint: disable=signature-differs The COURSE_CERT_AWARDED signal helps determine if a Program Certificate can be awarded to a learner in the Credentials IDA. """ + try: + self.user, self.course_id, self.mode, self.status = CertificateCreationRequested.run_filter( + user=self.user, course_id=self.course_id, mode=self.mode, status=self.status, + ) + except CertificateCreationRequested.PreventCertificateCreation as exc: + raise CertificateGenerationNotAllowed(str(exc)) from exc + super().save(*args, **kwargs) COURSE_CERT_CHANGED.send_robust( sender=self.__class__, diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 287d87f7c8af..c201898c736a 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -18,6 +18,7 @@ from eventtracking import tracker from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey +from openedx_filters.learning.filters import CertificateRenderStarted from organizations import api as organizations_api from edx_django_utils.plugins import pluggable_override @@ -643,6 +644,13 @@ def render_html_view(request, course_id, certificate=None): # Track certificate view events _track_certificate_events(request, course, user, user_certificate) + try: + context, custom_template = CertificateRenderStarted.run_filter( + context=context, custom_template=custom_template, + ) + except CertificateRenderStarted.PreventCertificateRender: + return _render_invalid_certificate(request, course_id, platform_name, configuration) + # Render the certificate return _render_valid_certificate(request, context, custom_template) diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 0ea412181961..6c53f108184b 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -38,6 +38,7 @@ from markupsafe import escape from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey +from openedx_filters.learning.filters import CourseAboutRenderStarted from pytz import UTC from requests.exceptions import ConnectionError, Timeout # pylint: disable=redefined-builtin from rest_framework import status @@ -1025,7 +1026,15 @@ def course_about(request, course_id): 'allow_anonymous': allow_anonymous, } - return render_to_response('courseware/course_about.html', context) + course_about_template = 'courseware/course_about.html' + try: + context, course_about_template = CourseAboutRenderStarted.run_filter( + context=context, template_name=course_about_template, + ) + except CourseAboutRenderStarted.PreventCourseAboutRender as exc: + raise CourseAccessRedirect(reverse(exc.redirect_to or 'dashboard')) from exc + + return render_to_response(course_about_template, context) @ensure_csrf_cookie diff --git a/openedx/core/djangoapps/course_groups/models.py b/openedx/core/djangoapps/course_groups/models.py index da861f06b606..6eb0e1e94772 100644 --- a/openedx/core/djangoapps/course_groups/models.py +++ b/openedx/core/djangoapps/course_groups/models.py @@ -13,15 +13,23 @@ from django.dispatch import receiver from opaque_keys.edx.django.models import CourseKeyField +from openedx_filters.learning.filters import CohortChangeRequested from openedx.core.djangolib.model_mixins import DeletableByUserValue from openedx_events.learning.data import CohortData, CourseData, UserData, UserPersonalData # lint-amnesty, pylint: disable=wrong-import-order from openedx_events.learning.signals import COHORT_MEMBERSHIP_CHANGED # lint-amnesty, pylint: disable=wrong-import-order - log = logging.getLogger(__name__) +class CohortMembershipException(Exception): + pass + + +class CohortChangeNotAllowed(CohortMembershipException): + pass + + class CourseUserGroup(models.Model): """ This model represents groups of users in a course. Groups may have different types, @@ -122,6 +130,14 @@ def assign(cls, cohort, user): cohort_name=cohort.name)) else: previous_cohort = membership.course_user_group + + try: + membership, cohort = CohortChangeRequested.run_filter( + current_membership=membership, target_cohort=cohort, + ) + except CohortChangeRequested.PreventCohortChange as exc: + raise CohortChangeNotAllowed(str(exc)) from exc + previous_cohort.users.remove(user) membership.course_user_group = cohort diff --git a/openedx/features/course_experience/views/course_home.py b/openedx/features/course_experience/views/course_home.py index e488936d7bc9..805bd5c3aece 100644 --- a/openedx/features/course_experience/views/course_home.py +++ b/openedx/features/course_experience/views/course_home.py @@ -11,6 +11,7 @@ from django.views.decorators.cache import cache_control from django.views.decorators.csrf import ensure_csrf_cookie from opaque_keys.edx.keys import CourseKey +from openedx_filters.learning.filters import CourseHomeRenderStarted from web_fragments.fragment import Fragment from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active @@ -240,5 +241,14 @@ def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty 'has_discount': has_discount, 'show_search': show_search, } - html = render_to_string('course_experience/course-home-fragment.html', context) + + course_home_template = 'course_experience/course-home-fragment.html' + try: + context, course_home_template = CourseHomeRenderStarted.run_filter( + context=context, template_name=course_home_template, + ) + except CourseHomeRenderStarted.PreventCourseHomeRender as exc: + raise CourseAccessRedirect(reverse(exc.redirect_to or 'dashboard')) from exc + + html = render_to_string(course_home_template, context) return Fragment(html) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 9f8096962ebb..1b64752d2938 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -702,7 +702,7 @@ openedx-calc==2.0.1 # -r requirements/edx/base.in openedx-events==0.7.1 # via -r requirements/edx/base.in -openedx-filters==0.4.3 +git+https://github.com/eduNEXT/openedx-filters.git@MJG/2nd_filters_batch#egg=openedx_filters==0.5.0_gamma # via -r requirements/edx/base.in ora2==3.8.1 # via -r requirements/edx/base.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index f4339914425a..a46732b83243 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -937,7 +937,7 @@ openedx-calc==2.0.1 # -r requirements/edx/testing.txt openedx-events==0.7.1 # via -r requirements/edx/testing.txt -openedx-filters==0.4.3 +git+https://github.com/eduNEXT/openedx-filters.git@MJG/2nd_filters_batch#egg=openedx_filters==0.5.0_gamma # via -r requirements/edx/testing.txt ora2==3.8.1 # via -r requirements/edx/testing.txt diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index c8928413f559..ccc9f23e50cf 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -887,7 +887,7 @@ openedx-calc==2.0.1 # -r requirements/edx/base.txt openedx-events==0.7.1 # via -r requirements/edx/base.txt -openedx-filters==0.4.3 +git+https://github.com/eduNEXT/openedx-filters.git@MJG/2nd_filters_batch#egg=openedx_filters==0.5.0_gamma # via -r requirements/edx/base.txt ora2==3.8.1 # via -r requirements/edx/base.txt