From 4b94d6367aef6c3880bc72f9e6a82ad93b77803e Mon Sep 17 00:00:00 2001 From: Bill DeRusha Date: Mon, 30 Nov 2015 09:59:40 -0500 Subject: [PATCH] Update dashboard banners and track selection for audit track Remove cert messaging from audit cert/grade info partial move enrollment display method to helpers --- common/djangoapps/course_modes/helpers.py | 86 +++++++++++++ common/djangoapps/course_modes/models.py | 113 ++++-------------- .../course_modes/tests/test_models.py | 32 +++-- common/djangoapps/course_modes/views.py | 10 +- .../student/tests/test_enrollment.py | 11 +- .../student/tests/test_verification_status.py | 15 ++- common/djangoapps/student/tests/tests.py | 14 ++- common/djangoapps/student/views.py | 8 +- common/templates/course_modes/choose.html | 26 +++- .../_dashboard_certificate_information.html | 10 +- .../dashboard/_dashboard_course_listing.html | 11 +- 11 files changed, 209 insertions(+), 127 deletions(-) diff --git a/common/djangoapps/course_modes/helpers.py b/common/djangoapps/course_modes/helpers.py index e69de29bb2..4a3ceae231 100644 --- a/common/djangoapps/course_modes/helpers.py +++ b/common/djangoapps/course_modes/helpers.py @@ -0,0 +1,86 @@ +""" Helper methods for CourseModes. """ +from django.utils.translation import ugettext_lazy as _ + +from course_modes.models import CourseMode +from student.helpers import ( + VERIFY_STATUS_NEED_TO_VERIFY, + VERIFY_STATUS_SUBMITTED, + VERIFY_STATUS_APPROVED +) + +DISPLAY_VERIFIED = "verified" +DISPLAY_HONOR = "honor" +DISPLAY_AUDIT = "audit" +DISPLAY_PROFESSIONAL = "professional" + + +def enrollment_mode_display(mode, verification_status, course_id): + """ Select appropriate display strings and CSS classes. + + Uses mode and verification status to select appropriate display strings and CSS classes + for certificate display. + + Args: + mode (str): enrollment mode. + verification_status (str) : verification status of student + + Returns: + dictionary: + """ + show_image = False + image_alt = '' + enrollment_title = '' + enrollment_value = '' + display_mode = _enrollment_mode_display(mode, verification_status, course_id) + + if display_mode == DISPLAY_VERIFIED: + if verification_status in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]: + enrollment_title = _("Your verification is pending") + enrollment_value = _("Verified: Pending Verification") + show_image = True + image_alt = _("ID verification pending") + elif verification_status == VERIFY_STATUS_APPROVED: + enrollment_title = _("You're enrolled as a verified student") + enrollment_value = _("Verified") + show_image = True + image_alt = _("ID Verified Ribbon/Badge") + elif display_mode == DISPLAY_HONOR: + enrollment_title = _("You're enrolled as an honor code student") + enrollment_value = _("Honor Code") + elif display_mode == DISPLAY_PROFESSIONAL: + enrollment_title = _("You're enrolled as a professional education student") + enrollment_value = _("Professional Ed") + + return { + 'enrollment_title': unicode(enrollment_title), + 'enrollment_value': unicode(enrollment_value), + 'show_image': show_image, + 'image_alt': unicode(image_alt), + 'display_mode': _enrollment_mode_display(mode, verification_status, course_id) + } + + +def _enrollment_mode_display(enrollment_mode, verification_status, course_id): + """Checking enrollment mode and status and returns the display mode + Args: + enrollment_mode (str): enrollment mode. + verification_status (str) : verification status of student + + Returns: + display_mode (str) : display mode for certs + """ + course_mode_slugs = [mode.slug for mode in CourseMode.modes_for_course(course_id)] + + if enrollment_mode == CourseMode.VERIFIED: + if verification_status in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED]: + display_mode = DISPLAY_VERIFIED + elif DISPLAY_HONOR in course_mode_slugs: + display_mode = DISPLAY_HONOR + else: + display_mode = DISPLAY_AUDIT + elif enrollment_mode in [CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE]: + display_mode = DISPLAY_PROFESSIONAL + else: + display_mode = enrollment_mode + + return display_mode diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 1eb6a529ec..fa42c05c87 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -506,8 +506,27 @@ class CourseMode(models.Model): if cls.is_white_label(course_id, modes_dict=modes_dict): return False - # Check that the default mode is available. - return cls.DEFAULT_MODE_SLUG in modes_dict + # Check that a free mode is available. + return cls.AUDIT in modes_dict or cls.HONOR in modes_dict + + @classmethod + def auto_enroll_mode(cls, course_id, modes_dict=None): + """ + return the auto-enrollable mode from given dict + + Args: + modes_dict (dict): course modes. + + Returns: + String: Mode name + """ + if modes_dict is None: + modes_dict = cls.modes_for_course_dict(course_id) + + if cls.HONOR in modes_dict: + return cls.HONOR + elif cls.AUDIT in modes_dict: + return cls.AUDIT @classmethod def is_white_label(cls, course_id, modes_dict=None): @@ -547,96 +566,6 @@ class CourseMode(models.Model): modes = cls.modes_for_course(course_id) return min(mode.min_price for mode in modes if mode.currency.lower() == currency.lower()) - @classmethod - def enrollment_mode_display(cls, mode, verification_status): - """ Select appropriate display strings and CSS classes. - - Uses mode and verification status to select appropriate display strings and CSS classes - for certificate display. - - Args: - mode (str): enrollment mode. - verification_status (str) : verification status of student - - Returns: - dictionary: - """ - - # import inside the function to avoid the circular import - from student.helpers import ( - VERIFY_STATUS_NEED_TO_VERIFY, - VERIFY_STATUS_SUBMITTED, - VERIFY_STATUS_APPROVED - ) - - show_image = False - image_alt = '' - - if mode == cls.VERIFIED: - if verification_status in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED]: - enrollment_title = _("Your verification is pending") - enrollment_value = _("Verified: Pending Verification") - show_image = True - image_alt = _("ID verification pending") - elif verification_status == VERIFY_STATUS_APPROVED: - enrollment_title = _("You're enrolled as a verified student") - enrollment_value = _("Verified") - show_image = True - image_alt = _("ID Verified Ribbon/Badge") - else: - enrollment_title = _("You're enrolled as an honor code student") - enrollment_value = _("Honor Code") - elif mode == cls.HONOR: - enrollment_title = _("You're enrolled as an honor code student") - enrollment_value = _("Honor Code") - elif mode == cls.AUDIT: - enrollment_title = _("You're auditing this course") - enrollment_value = _("Auditing") - elif mode in [cls.PROFESSIONAL, cls.NO_ID_PROFESSIONAL_MODE]: - enrollment_title = _("You're enrolled as a professional education student") - enrollment_value = _("Professional Ed") - else: - enrollment_title = '' - enrollment_value = '' - - return { - 'enrollment_title': unicode(enrollment_title), - 'enrollment_value': unicode(enrollment_value), - 'show_image': show_image, - 'image_alt': unicode(image_alt), - 'display_mode': cls._enrollment_mode_display(mode, verification_status) - } - - @staticmethod - def _enrollment_mode_display(enrollment_mode, verification_status): - """Checking enrollment mode and status and returns the display mode - Args: - enrollment_mode (str): enrollment mode. - verification_status (str) : verification status of student - - Returns: - display_mode (str) : display mode for certs - """ - - # import inside the function to avoid the circular import - from student.helpers import ( - VERIFY_STATUS_NEED_TO_VERIFY, - VERIFY_STATUS_SUBMITTED, - VERIFY_STATUS_APPROVED - ) - - if enrollment_mode == CourseMode.VERIFIED: - if verification_status in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED]: - display_mode = "verified" - else: - display_mode = "honor" - elif enrollment_mode in [CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE]: - display_mode = "professional" - else: - display_mode = enrollment_mode - - return display_mode - def to_tuple(self): """ Takes a mode model and turns it into a model named tuple. diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 69de9de4a8..24a0dc3fde 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -15,6 +15,7 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locator import CourseLocator import pytz +from course_modes.helpers import enrollment_mode_display from course_modes.models import CourseMode, Mode @@ -193,6 +194,21 @@ class CourseModeModelTest(TestCase): # Verify that we can or cannot auto enroll self.assertEqual(CourseMode.can_auto_enroll(self.course_key), can_auto_enroll) + @ddt.data( + ([], None), + (["honor", "audit", "verified"], "honor"), + (["honor", "audit"], "honor"), + (["audit", "verified"], "audit"), + (["professional"], None), + (["no-id-professional"], None), + (["credit", "audit", "verified"], "audit"), + (["credit"], None), + ) + @ddt.unpack + def test_auto_enroll_mode(self, modes, result): + # Verify that the proper auto enroll mode is returned + self.assertEqual(CourseMode.auto_enroll_mode(self.course_key, modes), result) + def test_all_modes_for_courses(self): now = datetime.now(pytz.UTC) future = now + timedelta(days=1) @@ -316,30 +332,30 @@ class CourseModeModelTest(TestCase): def test_enrollment_mode_display(self, mode, verification_status): if mode == "verified": self.assertEqual( - CourseMode.enrollment_mode_display(mode, verification_status), + enrollment_mode_display(mode, verification_status, self.course_key), self._enrollment_display_modes_dicts(verification_status) ) self.assertEqual( - CourseMode.enrollment_mode_display(mode, verification_status), + enrollment_mode_display(mode, verification_status, self.course_key), self._enrollment_display_modes_dicts(verification_status) ) self.assertEqual( - CourseMode.enrollment_mode_display(mode, verification_status), + enrollment_mode_display(mode, verification_status, self.course_key), self._enrollment_display_modes_dicts(verification_status) ) elif mode == "honor": self.assertEqual( - CourseMode.enrollment_mode_display(mode, verification_status), + enrollment_mode_display(mode, verification_status, self.course_key), self._enrollment_display_modes_dicts(mode) ) elif mode == "audit": self.assertEqual( - CourseMode.enrollment_mode_display(mode, verification_status), + enrollment_mode_display(mode, verification_status, self.course_key), self._enrollment_display_modes_dicts(mode) ) elif mode == "professional": self.assertEqual( - CourseMode.enrollment_mode_display(mode, verification_status), + enrollment_mode_display(mode, verification_status, self.course_key), self._enrollment_display_modes_dicts(mode) ) @@ -375,9 +391,9 @@ class CourseModeModelTest(TestCase): 'ID verification pending', 'verified'], "verify_approved": ["You're enrolled as a verified student", "Verified", True, 'ID Verified Ribbon/Badge', 'verified'], - "verify_none": ["You're enrolled as an honor code student", "Honor Code", False, '', 'honor'], + "verify_none": ["", "", False, '', 'audit'], "honor": ["You're enrolled as an honor code student", "Honor Code", False, '', 'honor'], - "audit": ["You're auditing this course", "Auditing", False, '', 'audit'], + "audit": ["", "", False, '', 'audit'], "professional": ["You're enrolled as a professional education student", "Professional Ed", False, '', 'professional'] } diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 331ecbd38a..58dbf8839c 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -174,12 +174,16 @@ class ChooseModeView(View): if requested_mode not in allowed_modes: return HttpResponseBadRequest(_("Enrollment mode not supported")) - if requested_mode == 'honor': - # The user will have already been enrolled in the honor mode at this + if requested_mode == 'audit': + # The user will have already been enrolled in the audit mode at this # point, so we just redirect them to the dashboard, thereby avoiding # hitting the database a second time attempting to enroll them. return redirect(reverse('dashboard')) + if requested_mode == 'honor': + CourseEnrollment.enroll(user, course_key, mode=requested_mode) + return redirect(reverse('dashboard')) + mode_info = allowed_modes[requested_mode] if requested_mode == 'verified': @@ -224,6 +228,8 @@ class ChooseModeView(View): return 'verified' if 'honor_mode' in request_dict: return 'honor' + if 'audit_mode' in request_dict: + return 'audit' else: return None diff --git a/common/djangoapps/student/tests/test_enrollment.py b/common/djangoapps/student/tests/test_enrollment.py index e8c26cdb62..679a86e12b 100644 --- a/common/djangoapps/student/tests/test_enrollment.py +++ b/common/djangoapps/student/tests/test_enrollment.py @@ -45,10 +45,17 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase): # and automatically enrolled ([], '', CourseMode.DEFAULT_MODE_SLUG), - # Audit / Verified / Honor + # Audit / Verified # We should always go to the "choose your course" page. # We should also be enrolled as the default mode. - (['honor', 'verified', 'audit'], 'course_modes_choose', CourseMode.DEFAULT_MODE_SLUG), + (['verified', 'audit'], 'course_modes_choose', CourseMode.DEFAULT_MODE_SLUG), + + # Audit / Verified / Honor + # We should always go to the "choose your course" page. + # We should also be enrolled as the honor mode. + # Since honor and audit are currently offered together this precedence must + # be maintained. + (['honor', 'verified', 'audit'], 'course_modes_choose', CourseMode.HONOR), # Professional ed # Expect that we're sent to the "choose your track" page diff --git a/common/djangoapps/student/tests/test_verification_status.py b/common/djangoapps/student/tests/test_verification_status.py index c3002bb63d..498a147fa2 100644 --- a/common/djangoapps/student/tests/test_verification_status.py +++ b/common/djangoapps/student/tests/test_verification_status.py @@ -44,7 +44,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): self.dashboard_url = reverse('dashboard') def test_enrolled_as_non_verified(self): - self._setup_mode_and_enrollment(None, "honor") + self._setup_mode_and_enrollment(None, "audit") # Expect that the course appears on the dashboard # without any verification messaging @@ -290,12 +290,9 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): VerificationDeadline.set_deadline(self.course.id, deadline) BANNER_ALT_MESSAGES = { - None: "Honor", VERIFY_STATUS_NEED_TO_VERIFY: "ID verification pending", VERIFY_STATUS_SUBMITTED: "ID verification pending", VERIFY_STATUS_APPROVED: "ID Verified Ribbon/Badge", - VERIFY_STATUS_MISSED_DEADLINE: "Honor", - VERIFY_STATUS_NEED_TO_REVERIFY: "Honor" } NOTIFICATION_MESSAGES = { @@ -309,12 +306,12 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): } MODE_CLASSES = { - None: "honor", + None: "audit", VERIFY_STATUS_NEED_TO_VERIFY: "verified", VERIFY_STATUS_SUBMITTED: "verified", VERIFY_STATUS_APPROVED: "verified", - VERIFY_STATUS_MISSED_DEADLINE: "honor", - VERIFY_STATUS_NEED_TO_REVERIFY: "honor" + VERIFY_STATUS_MISSED_DEADLINE: "audit", + VERIFY_STATUS_NEED_TO_REVERIFY: "audit" } def _assert_course_verification_status(self, status): @@ -334,7 +331,9 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase): self.assertContains(response, unicode(self.course.id)) # Verify that the correct banner is rendered on the dashboard - self.assertContains(response, self.BANNER_ALT_MESSAGES[status]) + alt_text = self.BANNER_ALT_MESSAGES.get(status) + if alt_text: + self.assertContains(response, alt_text) # Verify that the correct banner color is rendered self.assertContains( diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index a4bcacd915..689802f969 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -11,6 +11,7 @@ from urlparse import urljoin import pytz from mock import Mock, patch from opaque_keys.edx.locations import SlashSeparatedCourseKey +from pyquery import PyQuery as pq from django.conf import settings from django.contrib.auth.models import User, AnonymousUser @@ -249,7 +250,7 @@ class DashboardTest(ModuleStoreTestCase): self.client.login(username="jack", password="test") self._check_verification_status_on('verified', 'You\'re enrolled as a verified student') self._check_verification_status_on('honor', 'You\'re enrolled as an honor code student') - self._check_verification_status_on('audit', 'You\'re auditing this course') + self._check_verification_status_off('audit', '') self._check_verification_status_on('professional', 'You\'re enrolled as a professional education student') self._check_verification_status_on('no-id-professional', 'You\'re enrolled as a professional education student') @@ -269,8 +270,13 @@ class DashboardTest(ModuleStoreTestCase): attempt.approve() response = self.client.get(reverse('dashboard')) - self.assertNotContains(response, "class=\"course {0}\"".format(mode)) - self.assertNotContains(response, value) + + if mode == 'audit': + # Audit mode does not have a banner. Assert no banner element. + self.assertEqual(pq(response.content)(".sts-enrollment").length, 0) + else: + self.assertNotContains(response, "class=\"course {0}\"".format(mode)) + self.assertNotContains(response, value) @patch.dict("django.conf.settings.FEATURES", {'ENABLE_VERIFIED_CERTIFICATES': False}) def test_verification_status_invisible(self): @@ -281,7 +287,7 @@ class DashboardTest(ModuleStoreTestCase): self.client.login(username="jack", password="test") self._check_verification_status_off('verified', 'You\'re enrolled as a verified student') self._check_verification_status_off('honor', 'You\'re enrolled as an honor code student') - self._check_verification_status_off('audit', 'You\'re auditing this course') + self._check_verification_status_off('audit', '') def test_course_mode_info(self): verified_mode = CourseModeFactory.create( diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index f3383015e9..db8d08e354 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1011,14 +1011,16 @@ def change_enrollment(request, check_access=True): # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): - # Enroll the user using the default mode (honor) + # Enroll the user using the default mode (audit) # We're assuming that users of the course enrollment table # will NOT try to look up the course enrollment model # by its slug. If they do, it's possible (based on the state of the database) # for no such model to exist, even though we've set the enrollment type - # to "honor". + # to "audit". try: - CourseEnrollment.enroll(user, course_id, check_access=check_access) + enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes) + if enroll_mode: + CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) except Exception: return HttpResponseBadRequest(_("Could not enroll")) diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index 0def3743fd..3b05aa7f41 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -143,15 +143,35 @@ from django.core.urlresolvers import reverse
-

${_("Audit This Course")}

+

${_("Earn an Honor Certificate")}

-

${_("Audit this course for free and have complete access to all the course material, activities, tests, and forums. If your work is satisfactory and you abide by the Honor Code, you'll receive a personalized Honor Code Certificate to showcase your achievement.")}

+

${_("Take this course for free and have complete access to all the course material, activities, tests, and forums. Please note that learners who earn a passing grade will earn a certificate in this course.")}

+
+ % elif "audit" in modes: + + ${_("or")} + + +
+
+ +

${_("Audit This Course")}

+
+

${_("Audit this course for free and have complete access to all the course material, activities, tests, and forums. Please note that this track does not offer a certificate for learners who earn a passing grade.")}

+
+
+ +
diff --git a/lms/templates/dashboard/_dashboard_certificate_information.html b/lms/templates/dashboard/_dashboard_certificate_information.html index 4d75650a81..bf6f716a5b 100644 --- a/lms/templates/dashboard/_dashboard_certificate_information.html +++ b/lms/templates/dashboard/_dashboard_certificate_information.html @@ -33,9 +33,13 @@ else: % elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):

${_("Your final grade:")} ${"{0:.0f}%".format(float(cert_status['grade'])*100)}. - % if cert_status['status'] == 'notpassing' and enrollment.mode != 'audit': - ${_("Grade required for a {cert_name_short}:").format(cert_name_short=cert_name_short)} - ${"{0:.0f}%".format(float(course_overview.lowest_passing_grade)*100)}. + % if cert_status['status'] == 'notpassing': + % if enrollment.mode != 'audit': + ${_("Grade required for a {cert_name_short}:").format(cert_name_short=cert_name_short)} + % else: + ${_("Grade required to pass this course:")} + % endif + ${"{0:.0f}%".format(float(course_overview.lowest_passing_grade)*100)}. % elif cert_status['status'] == 'restricted' and enrollment.mode == 'verified':

${_("Your verified {cert_name_long} is being held pending confirmation that the issuance of your {cert_name_short} is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting {email}. If you would like a refund on your {cert_name_long}, please contact our billing address {billing_email}").format(email='{email}.'.format(email=settings.CONTACT_EMAIL), billing_email='{email}'.format(email=settings.PAYMENT_SUPPORT_EMAIL), cert_name_short=cert_name_short, cert_name_long=cert_name_long)} diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index b17e089ca1..50e6b1ac1a 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -9,6 +9,7 @@ from django.core.urlresolvers import reverse from markupsafe import escape from courseware.courses import get_course_university_about_section from course_modes.models import CourseMode +from course_modes.helpers import enrollment_mode_display from student.helpers import ( VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, @@ -34,7 +35,13 @@ from student.helpers import (

  • % if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'): - <% course_verified_certs = CourseMode.enrollment_mode_display(enrollment.mode, verification_status.get('status')) %> + <% + course_verified_certs = enrollment_mode_display( + enrollment.mode, + verification_status.get('status'), + course_overview.id + ) + %> <% mode_class = course_verified_certs.get('display_mode', '') if mode_class != '': @@ -69,7 +76,7 @@ from student.helpers import ( ${_('{course_number} {course_name} Cover Image').format(course_number=course_overview.number, course_name=course_overview.display_name_with_default) | h} % endif - % if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES'): + % if settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES') and course_verified_certs.get('display_mode') != 'audit': ${_("Enrolled as: ")} % if course_verified_certs.get('show_image'):