From 3d062aab620e80a96dc4a1733322bfb3be152845 Mon Sep 17 00:00:00 2001 From: Christie Rice <8483753+crice100@users.noreply.github.com> Date: Thu, 1 Aug 2019 08:37:41 -0400 Subject: [PATCH] REVMI-354 Add enroll_in_course permission (#21243) * REVMI-354 Add enroll_in_course permission * Fix test --- common/djangoapps/course_modes/views.py | 6 +++--- common/djangoapps/student/models.py | 9 ++++----- lms/djangoapps/courseware/views/views.py | 3 ++- openedx/core/djangoapps/enrollments/permissions.py | 10 ++++++++++ openedx/core/djangoapps/programs/tests/test_utils.py | 12 ++++++------ openedx/core/djangoapps/programs/utils.py | 4 ++-- 6 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 openedx/core/djangoapps/enrollments/permissions.py diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 9d98ac9c66..6bcc521f83 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -28,12 +28,12 @@ from opaque_keys.edx.keys import CourseKey from six import text_type from course_modes.models import CourseMode -from courseware.access import has_access from edxmako.shortcuts import render_to_response from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.experiments.utils import get_experiment_user_metadata_context from openedx.core.djangoapps.catalog.utils import get_currency_data from openedx.core.djangoapps.embargo import api as embargo_api +from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.models import CourseDurationLimitConfig from openedx.features.course_experience.utils import get_first_purchase_offer_banner_fragment @@ -150,7 +150,7 @@ class ChooseModeView(View): # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible - # for univerity credit. + # for university credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging @@ -259,7 +259,7 @@ class ChooseModeView(View): # This is a bit redundant with logic in student.views.change_enrollment, # but I don't really have the time to refactor it more nicely and test. course = modulestore().get_course(course_key) - if not has_access(user, 'enroll', course): + if not user.has_perm(ENROLL_IN_COURSE, course): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 7cd56b94c7..a375daf2ac 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -1255,7 +1255,7 @@ class CourseEnrollment(models.Model): Arguments: user (User): the user for whom we want the program enrollment - coure_id (CourseKey): the id of the course the user has a course enrollment in + course_id (CourseKey): the id of the course the user has a course enrollment in Returns: ProgramEnrollment object or None @@ -1272,10 +1272,9 @@ class CourseEnrollment(models.Model): Returns a boolean value regarding whether the user has access to enroll in the course. Returns False if the enrollment has been closed. """ - # Disable the pylint error here, as per ormsbee. This local import was previously - # in CourseEnrollment.enroll - from courseware.access import has_access # pylint: disable=import-error - return not has_access(user, 'enroll', course) + # pylint: disable=import-error + from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE + return not user.has_perm(ENROLL_IN_COURSE, course) def update_enrollment(self, mode=None, is_active=None, skip_refund=False): """ diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py index 3650d3b628..f2bb818fcf 100644 --- a/lms/djangoapps/courseware/views/views.py +++ b/lms/djangoapps/courseware/views/views.py @@ -87,6 +87,7 @@ from openedx.core.djangoapps.credit.api import ( is_user_eligible_for_credit ) from openedx.core.djangoapps.enrollments.api import add_enrollment +from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.programs.utils import ProgramMarketingDataExtender @@ -869,7 +870,7 @@ def course_about(request, course_id): can_add_course_to_cart = _is_shopping_cart_enabled and registration_price and not ecommerce_checkout_link # Used to provide context to message to student if enrollment not allowed - can_enroll = bool(has_access(request.user, 'enroll', course)) + can_enroll = bool(request.user.has_perm(ENROLL_IN_COURSE, course)) invitation_only = course.invitation_only is_course_full = CourseEnrollment.objects.is_course_full(course) diff --git a/openedx/core/djangoapps/enrollments/permissions.py b/openedx/core/djangoapps/enrollments/permissions.py new file mode 100644 index 0000000000..85edfca43b --- /dev/null +++ b/openedx/core/djangoapps/enrollments/permissions.py @@ -0,0 +1,10 @@ +""" +Permission definitions for the enrollments djangoapp +""" + +from bridgekeeper import perms +from courseware.rules import HasAccessRule + +ENROLL_IN_COURSE = 'enrollment.enroll_in_course' + +perms[ENROLL_IN_COURSE] = HasAccessRule('enroll') diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 3dce58d302..a755ef79ed 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -335,7 +335,7 @@ class TestProgramProgressMeter(TestCase): self.assertEqual(meter.progress(count_only=True), expected) - def test_mutiple_program_enrollment(self, mock_get_programs): + def test_multiple_program_enrollment(self, mock_get_programs): """ Verify that correct programs are returned in the correct order when the user is enrolled in course runs appearing in programs. @@ -1162,7 +1162,7 @@ class TestProgramDataExtender(ModuleStoreTestCase): def test_learner_eligibility_for_one_click_purchase_user_entitlements(self): """ - Learner should be eligibile for one click purchase if they hold an entitlement in one or more courses + Learner should be eligible for one click purchase if they hold an entitlement in one or more courses in the program and there are remaining unpurchased courses in the program with entitlement products. """ course1 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True) @@ -1236,7 +1236,7 @@ class TestProgramDataExtender(ModuleStoreTestCase): """ course1 = _create_course(self, self.course_price) course2 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True) - # The above statement makes a verfied entitlement for the course, which is an applicable seat type + # The above statement makes a verified entitlement for the course, which is an applicable seat type # and the statement below makes a professional entitlement for the same course, which is not applicable course2['entitlements'].append(EntitlementFactory(mode=CourseMode.PROFESSIONAL)) expected_skus = set([course1['course_runs'][0]['seats'][0]['sku'], course2['entitlements'][0]['sku']]) @@ -1488,12 +1488,12 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): self.assertEqual(data['avg_price_per_course'], 0.0) @ddt.data(True, False) - @mock.patch(UTILS_MODULE + '.has_access') - def test_can_enroll(self, can_enroll, mock_has_access): + @mock.patch('django.contrib.auth.models.PermissionsMixin.has_perm') + def test_can_enroll(self, can_enroll, mock_has_perm): """ Verify that the student's can_enroll status is included. """ - mock_has_access.return_value = can_enroll + mock_has_perm.return_value = can_enroll data = ProgramMarketingDataExtender(self.program, self.user).extend() diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index de4118d810..f5fae79540 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -26,13 +26,13 @@ from entitlements.models import CourseEntitlement from lms.djangoapps.certificates import api as certificate_api from lms.djangoapps.certificates.models import GeneratedCertificate from lms.djangoapps.commerce.utils import EcommerceService -from lms.djangoapps.courseware.access import has_access from lms.djangoapps.grades.api import CourseGradeFactory from openedx.core.djangoapps.catalog.utils import get_fulfillable_course_runs_for_entitlement, get_programs from openedx.core.djangoapps.certificates.api import available_date_for_certificate from openedx.core.djangoapps.commerce.utils import ecommerce_api_client from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.credentials.utils import get_credentials +from openedx.core.djangoapps.enrollments.permissions import ENROLL_IN_COURSE from openedx.core.djangoapps.programs import ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from student.models import CourseEnrollment @@ -841,7 +841,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender): return {name for name in chain(cls.__dict__, ProgramDataExtender.__dict__) if name.startswith(prefix)} def _attach_course_run_can_enroll(self, run_mode): - run_mode['can_enroll'] = bool(has_access(self.user, 'enroll', self.course_overview)) + run_mode['can_enroll'] = bool(self.user.has_perm(ENROLL_IN_COURSE, self.course_overview)) def _attach_course_run_certificate_url(self, run_mode): """