diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index ccffd47568cb4dd30bf814d77c1fd7f7b4c58c7d..2bd63ccf0daf88ff849e7d7d5e1410d1c261f824 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -112,6 +112,21 @@ class CourseMode(models.Model): else: return None + @classmethod + def verified_mode_for_course(cls, course_id): + """ + Since we have two separate modes that can go through the verify flow, + we want to be able to select the 'correct' verified mode for a given course. + + Currently, we prefer to return the professional mode over the verified one + if both exist for the given course. + """ + modes_dict = cls.modes_for_course_dict(course_id) + verified_mode = modes_dict.get('verified', None) + professional_mode = modes_dict.get('professional', None) + # we prefer professional over verify + return professional_mode if professional_mode else verified_mode + @classmethod def min_course_price_for_verified_for_currency(cls, course_id, currency): """ diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 933faf8c6635ce12d0bd3341cf339638b323b8e6..c369aaaf0f51d38a77f2a412471b3f17043c82f5 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -113,3 +113,17 @@ class CourseModeModelTest(TestCase): modes = CourseMode.modes_for_course(SlashSeparatedCourseKey('TestOrg', 'TestCourse', 'TestRun')) self.assertEqual([CourseMode.DEFAULT_MODE], modes) + + def test_verified_mode_for_course(self): + self.create_mode('verified', 'Verified Certificate') + + mode = CourseMode.verified_mode_for_course(self.course_key) + + self.assertEqual(mode.slug, 'verified') + + # verify that the professional mode is preferred + self.create_mode('professional', 'Professional Education Verified Certificate') + + mode = CourseMode.verified_mode_for_course(self.course_key) + + self.assertEqual(mode.slug, 'professional') diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index a773c77f623cb47425dc0b7ee26c5055dea5d532..0aa242b4b2d7098dbdd97d81b57178960993d452 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -59,8 +59,6 @@ class ChooseModeView(View): ) ) - - donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(course_key, None) @@ -135,11 +133,6 @@ class ChooseModeView(View): donation_for_course = request.session.get("donation_for_course", {}) donation_for_course[course_key] = amount_value request.session["donation_for_course"] = donation_for_course - if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): - return redirect( - reverse('verify_student_verified', - kwargs={'course_id': course_key.to_deprecated_string()}) + "?upgrade={}".format(upgrade) - ) return redirect( reverse('verify_student_show_requirements', diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index eda01f7c17490abfd094f896ad8a4f91f16f4162..47d5d327f1582988db5645709f311effcd5b749e 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -701,8 +701,13 @@ class CertificateItem(OrderItem): item.qty = 1 item.unit_cost = cost course_name = modulestore().get_course(course_id).display_name - item.line_desc = _("Certificate of Achievement, {mode_name} for course {course}").format(mode_name=mode_info.name, - course=course_name) + # Translators: In this particular case, mode_name refers to a + # particular mode (i.e. Honor Code Certificate, Verified Certificate, etc) + # by which a user could enroll in the given course. + item.line_desc = _("{mode_name} for course {course}").format( + mode_name=mode_info.name, + course=course_name + ) item.currency = currency order.currency = currency order.save() @@ -725,7 +730,7 @@ class CertificateItem(OrderItem): @property def single_item_receipt_template(self): - if self.mode == 'verified': + if self.mode in ('verified', 'professional'): return 'shoppingcart/verified_cert_receipt.html' else: return super(CertificateItem, self).single_item_receipt_template diff --git a/lms/djangoapps/shoppingcart/tests/test_reports.py b/lms/djangoapps/shoppingcart/tests/test_reports.py index 22ae61fb1944efbc6ee5cfd53cef5b93f3d4c45a..e3f69dee9cbd874d52fce014ea6612e6a51c22c5 100644 --- a/lms/djangoapps/shoppingcart/tests/test_reports.py +++ b/lms/djangoapps/shoppingcart/tests/test_reports.py @@ -222,7 +222,7 @@ class ItemizedPurchaseReportTest(ModuleStoreTestCase): self.CORRECT_CSV = dedent(""" Purchase Time,Order ID,Status,Quantity,Unit Cost,Total Cost,Currency,Description,Comments {time_str},1,purchased,1,40,40,usd,Registration for Course: Robot Super Course,Ba\xc3\xbc\xe5\x8c\x85 - {time_str},1,purchased,1,40,40,usd,"Certificate of Achievement, verified cert for course Robot Super Course", + {time_str},1,purchased,1,40,40,usd,verified cert for course Robot Super Course, """.format(time_str=str(self.now))) def test_purchased_items_btw_dates(self): diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index f87033441c198ce313f96de242d7e6b0e7770ca8..0400f7c269ee1362bd5332ae7d4ccc0079727eea 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -74,18 +74,27 @@ class VerifyView(View): # bookkeeping-wise just to start over. progress_state = "start" - modes_dict = CourseMode.modes_for_course_dict(course_id) - verify_mode = modes_dict.get('verified', None) + # we prefer professional over verify + current_mode = CourseMode.verified_mode_for_course(course_id) + # if the course doesn't have a verified mode, we want to kick them # from the flow - if not verify_mode: + if not current_mode: return redirect(reverse('dashboard')) if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}): chosen_price = request.session["donation_for_course"][course_id.to_deprecated_string()] else: - chosen_price = verify_mode.min_price + chosen_price = current_mode.min_price course = modulestore().get_course(course_id) + if current_mode.suggested_prices != '': + suggested_prices = [ + decimal.Decimal(price) + for price in current_mode.suggested_prices.split(",") + ] + else: + suggested_prices = [] + context = { "progress_state": progress_state, "user_full_name": request.user.profile.name, @@ -95,15 +104,13 @@ class VerifyView(View): "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), - "suggested_prices": [ - decimal.Decimal(price) - for price in verify_mode.suggested_prices.split(",") - ], - "currency": verify_mode.currency.upper(), + "suggested_prices": suggested_prices, + "currency": current_mode.currency.upper(), "chosen_price": chosen_price, - "min_price": verify_mode.min_price, + "min_price": current_mode.min_price, "upgrade": upgrade == u'True', - "can_audit": "audit" in modes_dict, + "can_audit": CourseMode.mode_for_course(course_id, 'audit') is not None, + "modes_dict": CourseMode.modes_for_course_dict(course_id), } return render_to_response('verify_student/photo_verification.html', context) @@ -124,19 +131,20 @@ class VerifiedView(View): if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): return redirect(reverse('dashboard')) + modes_dict = CourseMode.modes_for_course_dict(course_id) - verify_mode = modes_dict.get('verified', None) - if verify_mode is None: - return redirect(reverse('dashboard')) + # we prefer professional over verify + current_mode = CourseMode.verified_mode_for_course(course_id) - chosen_price = request.session.get( - "donation_for_course", - {} - ).get( - course_id.to_deprecated_string(), - verify_mode.min_price - ) + # if the course doesn't have a verified mode, we want to kick them + # from the flow + if not current_mode: + return redirect(reverse('dashboard')) + if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}): + chosen_price = request.session["donation_for_course"][course_id.to_deprecated_string()] + else: + chosen_price = current_mode.min_price course = modulestore().get_course(course_id) context = { @@ -146,11 +154,12 @@ class VerifiedView(View): "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), - "currency": verify_mode.currency.upper(), + "currency": current_mode.currency.upper(), "chosen_price": chosen_price, "create_order_url": reverse("verify_student_create_order"), "upgrade": upgrade == u'True', "can_audit": "audit" in modes_dict, + "modes_dict": modes_dict, } return render_to_response('verify_student/verified.html', context) @@ -185,19 +194,24 @@ def create_order(request): donation_for_course[course_id] = amount request.session['donation_for_course'] = donation_for_course - verified_mode = CourseMode.modes_for_course_dict(course_id).get('verified', None) + # prefer professional mode over verified_mode + current_mode = CourseMode.verified_mode_for_course(course_id) + + if current_mode.slug == 'professional': + amount = current_mode.min_price # make sure this course has a verified mode - if not verified_mode: + if not current_mode: return HttpResponseBadRequest(_("This course doesn't support verified certificates")) - if amount < verified_mode.min_price: + if amount < current_mode.min_price: return HttpResponseBadRequest(_("No selected price or selected price is below minimum.")) # I know, we should check this is valid. All kinds of stuff missing here cart = Order.get_cart_for_user(request.user) cart.clear() - CertificateItem.add_to_order(cart, course_id, amount, 'verified') + enrollment_mode = current_mode.slug + CertificateItem.add_to_order(cart, course_id, amount, enrollment_mode) params = get_signed_purchase_params(cart) @@ -288,12 +302,20 @@ def show_requirements(request, course_id): """ Show the requirements necessary for the verification flow. """ + # TODO: seems borked for professional; we're told we need to take photos even if there's a pending verification course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) + upgrade = request.GET.get('upgrade', False) if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): return redirect(reverse('dashboard')) + if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): + return redirect( + reverse('verify_student_verified', + kwargs={'course_id': course_id.to_deprecated_string()}) + "?upgrade={}".format(upgrade) + ) upgrade = request.GET.get('upgrade', False) course = modulestore().get_course(course_id) + modes_dict = CourseMode.modes_for_course_dict(course_id) context = { "course_id": course_id.to_deprecated_string(), "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id.to_deprecated_string()}), @@ -303,6 +325,7 @@ def show_requirements(request, course_id): "course_num": course.display_number_with_default, "is_not_active": not request.user.is_active, "upgrade": upgrade == u'True', + "modes_dict": modes_dict, } return render_to_response("verify_student/show_requirements.html", context) diff --git a/lms/templates/verify_student/photo_verification.html b/lms/templates/verify_student/photo_verification.html index 17170d04ef61af6caf520a1737a72554509822ad..f9338944a8f9a29fd7e5a4f2bd9c8c40468dcc6d 100644 --- a/lms/templates/verify_student/photo_verification.html +++ b/lms/templates/verify_student/photo_verification.html @@ -177,12 +177,13 @@ <dt class="faq-question">${_("What do you do with this picture?")}</dt> <dd class="faq-answer">${_("We only use it to verify your identity. It is not displayed anywhere.")}</dd> - <dt class="faq-question">${_("What if my camera isn't working?")}</dt> - - %if upgrade: - <dd class="faq-answer">${_("You can always continue to audit the course without verifying.")}</dd> - %else: - <dd class="faq-answer">${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='<a rel="external" href="{}">'.format(course_modes_choose_url), a_end="</a>")}</dd> + %if "professional" not in modes_dict: + <dt class="faq-question">${_("What if my camera isn't working?")}</dt> + %if upgrade: + <dd class="faq-answer">${_("You can always continue to audit the course without verifying.")}</dd> + %else: + <dd class="faq-answer">${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='<a rel="external" href="{}">'.format(course_modes_choose_url), a_end="</a>")}</dd> + %endif %endif </dl> </div> @@ -366,6 +367,7 @@ </ul> </li> + %if len(suggested_prices) > 0: <li class="review-task review-task-contribution"> <h4 class="title">${_("Check Your Contribution Level")}</h4> @@ -376,12 +378,28 @@ <%include file="/course_modes/_contribution.html" args="suggested_prices=suggested_prices, currency=currency, chosen_price=chosen_price, min_price=min_price"/> </li> + %else: + <li class="review-task review-task-contribution"> + <h4 class="title">${_("Your Course Total")}</h4> + <div class="copy"> + <p>${_("To complete your registration, you will need to pay:")}</p> + </div> + <ul class="list-fields contribution-options"> + <li class="field contribution-option"> + <span class="deco-denomination">$</span> + <span class="label-value">${chosen_price}</span> + <span class="denomination-name">${currency}</span> + </label> + </li> + </ul> + </li> + %endif </ol> </div> <nav class="nav-wizard"> <div class="prompt-verify"> - <h3 class="title">Before proceeding, please confirm that your details match</h3> + <h3 class="title">${_("Before proceeding, please confirm that your details match")}</h3> <p class="copy"> ${_("Once you verify your details match the requirements, you can move on to step 4, payment on our secure server.")}</p> diff --git a/lms/templates/verify_student/verified.html b/lms/templates/verify_student/verified.html index cb61684493a3debfc89daf53ab5168401e6d8ea6..8059db32f158c6365566357b84654d2ac8b92ea3 100644 --- a/lms/templates/verify_student/verified.html +++ b/lms/templates/verify_student/verified.html @@ -85,8 +85,11 @@ $(document).ready(function() { </div> <nav class="nav-wizard is-ready"> - + %if "professional" in modes_dict: + <span class="help help-inline price-value">${_("Your Course Total is $ ")} <strong>${chosen_price}</strong></span> + %else: <span class="help help-inline price-value">${_("You have decided to pay $ ")} <strong>${chosen_price}</strong></span> + %endif <ol class="wizard-steps"> <li class="wizard-step step-proceed">