From 6802a38a9d8a0fae61cf8a53cb591377406b3ae3 Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Thu, 7 Feb 2019 15:42:40 -0500 Subject: [PATCH 1/6] Added master's track Added mode option to course enrollment management command --- .../management/commands/enroll_user_in_course.py | 15 ++++++++++++--- lms/envs/common.py | 6 ++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/enrollment/management/commands/enroll_user_in_course.py b/common/djangoapps/enrollment/management/commands/enroll_user_in_course.py index 033ed26565..3f393c71c0 100644 --- a/common/djangoapps/enrollment/management/commands/enroll_user_in_course.py +++ b/common/djangoapps/enrollment/management/commands/enroll_user_in_course.py @@ -12,9 +12,10 @@ class Command(BaseCommand): Enroll a user into a course """ help = """ - This enrolls a user into a given course with the default mode (e.g., 'honor', 'audit', etc). + This enrolls a user into a given course User email and course ID are required. + Mode is optional. It defaults to the default mode (e.g., 'honor', 'audit', etc). example: # Enroll a user test@example.com into the demo course @@ -35,7 +36,14 @@ class Command(BaseCommand): '-c', '--course', nargs=1, required=True, - help='course ID to enroll the user in') + help='course ID to enroll the user in' + ) + parser.add_argument( + '-m', '--mode', + required=False, + default=None, + help='course mode to enroll the user in' + ) def handle(self, *args, **options): """ @@ -43,10 +51,11 @@ class Command(BaseCommand): """ email = options['email'][0] course = options['course'][0] + mode = options['mode'] user = User.objects.get(email=email) try: - add_enrollment(user.username, course) + add_enrollment(user.username, course, mode=mode) except CourseEnrollmentExistsError: # If the user is already enrolled in the course, do nothing. pass diff --git a/lms/envs/common.py b/lms/envs/common.py index ffbeff77b5..2a8db707e9 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3383,6 +3383,12 @@ COURSE_ENROLLMENT_MODES = { "display_name": _("Honor"), "min_price": 0 }, + "masters": { + "id": 7, + "slug": "masters", + "display_name": _("Master's"), + "min_price": 0 + }, } CONTENT_TYPE_GATE_GROUP_IDS = { From 436416d86dc67d8ec1c473e1a43f1916a39401f1 Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Wed, 13 Feb 2019 10:20:03 -0500 Subject: [PATCH 2/6] Added modes to coursemode model --- common/djangoapps/course_modes/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 9f80c4e733..91a517df98 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -142,6 +142,7 @@ class CourseMode(models.Model): AUDIT = 'audit' NO_ID_PROFESSIONAL_MODE = 'no-id-professional' CREDIT_MODE = 'credit' + MASTERS = 'masters' DEFAULT_MODE = Mode( settings.COURSE_MODE_DEFAULTS['slug'], @@ -156,19 +157,19 @@ class CourseMode(models.Model): ) DEFAULT_MODE_SLUG = settings.COURSE_MODE_DEFAULTS['slug'] - ALL_MODES = [AUDIT, CREDIT_MODE, HONOR, NO_ID_PROFESSIONAL_MODE, PROFESSIONAL, VERIFIED, ] + ALL_MODES = [AUDIT, CREDIT_MODE, HONOR, NO_ID_PROFESSIONAL_MODE, PROFESSIONAL, VERIFIED, MASTERS, ] # Modes utilized for audit/free enrollments AUDIT_MODES = [AUDIT, HONOR] # Modes that allow a student to pursue a verified certificate - VERIFIED_MODES = [VERIFIED, PROFESSIONAL] + VERIFIED_MODES = [VERIFIED, PROFESSIONAL, MASTERS] # Modes that allow a student to pursue a non-verified certificate NON_VERIFIED_MODES = [HONOR, AUDIT, NO_ID_PROFESSIONAL_MODE] # Modes that allow a student to earn credit with a university partner - CREDIT_MODES = [CREDIT_MODE] + CREDIT_MODES = [CREDIT_MODE, MASTERS] # Modes that are eligible to purchase credit CREDIT_ELIGIBLE_MODES = [VERIFIED, PROFESSIONAL, NO_ID_PROFESSIONAL_MODE] From d780908bbd4cb8710b3f4a92c8861c68586f0dab Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Wed, 13 Feb 2019 10:24:52 -0500 Subject: [PATCH 3/6] Define permission used in edx-proctoring --- lms/djangoapps/courseware/rules.py | 25 ++++++++++ lms/djangoapps/courseware/tests/test_rules.py | 48 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 lms/djangoapps/courseware/rules.py create mode 100644 lms/djangoapps/courseware/tests/test_rules.py diff --git a/lms/djangoapps/courseware/rules.py b/lms/djangoapps/courseware/rules.py new file mode 100644 index 0000000000..ce019ca574 --- /dev/null +++ b/lms/djangoapps/courseware/rules.py @@ -0,0 +1,25 @@ +""" +django-rules for courseware related features +""" +from __future__ import absolute_import + +from opaque_keys.edx.keys import CourseKey +from student.models import CourseEnrollment + +import rules + + +@rules.predicate +def is_verified_or_masters_track_exam(user, exam): + """ + Returns whether the user is in a verified or master's track + """ + course_id = CourseKey.from_string(exam['course_id']) + mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_id) + return is_active and mode in ('verified', 'masters') + + +# The edx_proctoring.api uses this permission to gate access to the +# proctored experience +can_take_proctored_exam = is_verified_or_masters_track_exam +rules.set_perm('edx_proctoring.can_take_proctored_exam', is_verified_or_masters_track_exam) diff --git a/lms/djangoapps/courseware/tests/test_rules.py b/lms/djangoapps/courseware/tests/test_rules.py new file mode 100644 index 0000000000..cd36ad0115 --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_rules.py @@ -0,0 +1,48 @@ +""" +Tests for permissions defined in courseware.rules +""" +import ddt + +from django.test import TestCase + +from course_modes.tests.factories import CourseModeFactory + +from opaque_keys.edx.locator import CourseLocator +from student.models import CourseEnrollment +from student.tests.factories import UserFactory + + +@ddt.ddt +class PermissionTests(TestCase): + """ + Tests for permissions defined in courseware.rules + """ + def setUp(self): + super(PermissionTests, self).setUp() + self.user = UserFactory() + + self.course_id = CourseLocator('MITx', '000', 'Perm_course') + CourseModeFactory(mode_slug='verified', course_id=self.course_id) + CourseModeFactory(mode_slug='masters', course_id=self.course_id) + + def tearDown(self): + super(PermissionTests, self).tearDown() + self.user.delete() + + @ddt.data( + (None, False), + ('audit', False), + ('verified', True), + ('masters', True), + ) + @ddt.unpack + def test_proctoring_perm(self, mode, should_have_perm): + """ + Test that the user has the edx_proctoring.can_take_proctored_exam permission + """ + if mode is not None: + CourseEnrollment.enroll(self.user, self.course_id, mode=mode) + else: + CourseEnrollment.unenroll(self.user, self.course_id) + has_perm = self.user.has_perm('edx_proctoring.can_take_proctored_exam', {'course_id': unicode(self.course_id)}) + assert has_perm == should_have_perm From 5dcc3d33e99deb1abf41f9a149237bf680ae8566 Mon Sep 17 00:00:00 2001 From: Matt Hughes Date: Fri, 15 Feb 2019 14:41:20 -0500 Subject: [PATCH 4/6] Add masters track to instr. dash. enrollment breakdown JIRA:EDUCATOR-4027 --- .../instructor/instructor_dashboard_2/course_info.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lms/templates/instructor/instructor_dashboard_2/course_info.html b/lms/templates/instructor/instructor_dashboard_2/course_info.html index 35f3aecabc..91e18660d1 100644 --- a/lms/templates/instructor/instructor_dashboard_2/course_info.html +++ b/lms/templates/instructor/instructor_dashboard_2/course_info.html @@ -24,6 +24,11 @@ from openedx.core.djangolib.markup import HTML, Text ${_("Professional")}${modes['professional'] + modes['no-id-professional']} + %if modes['masters'] > 0: + + ${_("Master's")}${modes['masters']} + + %endif ${_("Total")} From 1ebc6f9c5ee2d19053214404d40561a1f4600a05 Mon Sep 17 00:00:00 2001 From: Matt Hughes Date: Wed, 20 Feb 2019 11:52:41 -0500 Subject: [PATCH 5/6] Revert "Add masters track to instr. dash. enrollment breakdown" This reverts commit 5dcc3d33e99deb1abf41f9a149237bf680ae8566. --- .../instructor/instructor_dashboard_2/course_info.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lms/templates/instructor/instructor_dashboard_2/course_info.html b/lms/templates/instructor/instructor_dashboard_2/course_info.html index 91e18660d1..35f3aecabc 100644 --- a/lms/templates/instructor/instructor_dashboard_2/course_info.html +++ b/lms/templates/instructor/instructor_dashboard_2/course_info.html @@ -24,11 +24,6 @@ from openedx.core.djangolib.markup import HTML, Text ${_("Professional")}${modes['professional'] + modes['no-id-professional']} - %if modes['masters'] > 0: - - ${_("Master's")}${modes['masters']} - - %endif ${_("Total")} From 17f0a4fb521ad1200cbb12a9e400c5429f2febb6 Mon Sep 17 00:00:00 2001 From: Matt Hughes Date: Wed, 20 Feb 2019 12:40:53 -0500 Subject: [PATCH 6/6] Allow educators to partition masters students as content groups JIRA:EDUCATOR-4022 --- common/djangoapps/course_modes/models.py | 5 ++++- openedx/core/djangoapps/credentials/signals.py | 2 +- openedx/core/djangoapps/credentials/tests/test_signals.py | 2 ++ openedx/core/djangoapps/programs/tasks/v1/tasks.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 91a517df98..0f3b90c32b 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -169,11 +169,14 @@ class CourseMode(models.Model): NON_VERIFIED_MODES = [HONOR, AUDIT, NO_ID_PROFESSIONAL_MODE] # Modes that allow a student to earn credit with a university partner - CREDIT_MODES = [CREDIT_MODE, MASTERS] + CREDIT_MODES = [CREDIT_MODE] # Modes that are eligible to purchase credit CREDIT_ELIGIBLE_MODES = [VERIFIED, PROFESSIONAL, NO_ID_PROFESSIONAL_MODE] + # Modes for which certificates/programs may need to be updated + CERTIFICATE_RELEVANT_MODES = CREDIT_MODES + CREDIT_ELIGIBLE_MODES + [MASTERS] + # Modes that are allowed to upsell UPSELL_TO_VERIFIED_MODES = [HONOR, AUDIT] diff --git a/openedx/core/djangoapps/credentials/signals.py b/openedx/core/djangoapps/credentials/signals.py index 95a2a45550..dd9d0311fd 100644 --- a/openedx/core/djangoapps/credentials/signals.py +++ b/openedx/core/djangoapps/credentials/signals.py @@ -17,7 +17,7 @@ log = getLogger(__name__) # "interesting" here means "credentials will want to know about it" -INTERESTING_MODES = CourseMode.CREDIT_ELIGIBLE_MODES + CourseMode.CREDIT_MODES +INTERESTING_MODES = CourseMode.CERTIFICATE_RELEVANT_MODES INTERESTING_STATUSES = [ CertificateStatuses.notpassing, CertificateStatuses.downloadable, diff --git a/openedx/core/djangoapps/credentials/tests/test_signals.py b/openedx/core/djangoapps/credentials/tests/test_signals.py index 94e334a91b..b96aa5fdd1 100644 --- a/openedx/core/djangoapps/credentials/tests/test_signals.py +++ b/openedx/core/djangoapps/credentials/tests/test_signals.py @@ -43,6 +43,8 @@ class TestCredentialsSignalsSendGrade(TestCase): [True, 'no-id-professional', 'downloadable'], [True, 'credit', 'downloadable'], [True, 'verified', 'notpassing'], + [True, 'masters', 'downloadable'], + [True, 'masters', 'notpassing'], [False, 'audit', 'downloadable'], [False, 'professional', 'generating'], [False, 'no-id-professional', 'generating'], diff --git a/openedx/core/djangoapps/programs/tasks/v1/tasks.py b/openedx/core/djangoapps/programs/tasks/v1/tasks.py index 69c492bff4..24ced118c1 100644 --- a/openedx/core/djangoapps/programs/tasks/v1/tasks.py +++ b/openedx/core/djangoapps/programs/tasks/v1/tasks.py @@ -304,7 +304,7 @@ def award_course_certificate(self, username, course_run_key): username ) return - if certificate.mode in CourseMode.CREDIT_ELIGIBLE_MODES + CourseMode.CREDIT_MODES: + if certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES: try: course_overview = CourseOverview.get_from_id(course_key) except (CourseOverview.DoesNotExist, IOError):