diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index ce15059643131f97ed83d8df69874e42af16a0ce..8e475d1ec1c13c1593e60f5b7f15efed87ad71f3 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -9,7 +9,7 @@ from collections import namedtuple from django.utils.translation import ugettext as _ from django.db.models import Q -Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency']) +Mode = namedtuple('Mode', ['slug', 'name', 'min_price', 'suggested_prices', 'currency', 'expiration_date']) class CourseMode(models.Model): @@ -39,7 +39,7 @@ class CourseMode(models.Model): # turn this mode off after the given expiration date expiration_date = models.DateField(default=None, null=True, blank=True) - DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd') + DEFAULT_MODE = Mode('honor', _('Honor Code Certificate'), 0, '', 'usd', None) DEFAULT_MODE_SLUG = 'honor' class Meta: @@ -57,8 +57,14 @@ class CourseMode(models.Model): found_course_modes = cls.objects.filter(Q(course_id=course_id) & (Q(expiration_date__isnull=True) | Q(expiration_date__gte=now))) - modes = ([Mode(mode.mode_slug, mode.mode_display_name, mode.min_price, mode.suggested_prices, mode.currency) - for mode in found_course_modes]) + modes = ([Mode( + mode.mode_slug, + mode.mode_display_name, + mode.min_price, + mode.suggested_prices, + mode.currency, + mode.expiration_date + ) for mode in found_course_modes]) if not modes: modes = [cls.DEFAULT_MODE] return modes diff --git a/common/djangoapps/course_modes/tests/test_models.py b/common/djangoapps/course_modes/tests/test_models.py index 651c7c51a56d72d5f94ca86931b0d942b9d7d560..7a01c30dc4990a3d0d4ced8c7c7508046aec0eae 100644 --- a/common/djangoapps/course_modes/tests/test_models.py +++ b/common/djangoapps/course_modes/tests/test_models.py @@ -49,7 +49,7 @@ class CourseModeModelTest(TestCase): self.create_mode('verified', 'Verified Certificate') modes = CourseMode.modes_for_course(self.course_id) - mode = Mode(u'verified', u'Verified Certificate', 0, '', 'usd') + mode = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', None) self.assertEqual([mode], modes) modes_dict = CourseMode.modes_for_course_dict(self.course_id) @@ -61,8 +61,8 @@ class CourseModeModelTest(TestCase): """ Finding the modes when there's multiple modes """ - mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd') - mode2 = Mode(u'verified', u'Verified Certificate', 0, '', 'usd') + mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None) + mode2 = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', None) set_modes = [mode1, mode2] for mode in set_modes: self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices) @@ -81,9 +81,9 @@ class CourseModeModelTest(TestCase): self.assertEqual(0, CourseMode.min_course_price_for_currency(self.course_id, 'usd')) # create some modes - mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd') - mode2 = Mode(u'verified', u'Verified Certificate', 20, '', 'usd') - mode3 = Mode(u'honor', u'Honor Code Certificate', 80, '', 'cny') + mode1 = Mode(u'honor', u'Honor Code Certificate', 10, '', 'usd', None) + mode2 = Mode(u'verified', u'Verified Certificate', 20, '', 'usd', None) + mode3 = Mode(u'honor', u'Honor Code Certificate', 80, '', 'cny', None) set_modes = [mode1, mode2, mode3] for mode in set_modes: self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency) @@ -98,14 +98,15 @@ class CourseModeModelTest(TestCase): modes = CourseMode.modes_for_course(self.course_id) self.assertEqual([CourseMode.DEFAULT_MODE], modes) - mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd') + mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None) self.create_mode(mode1.slug, mode1.name, mode1.min_price, mode1.suggested_prices) modes = CourseMode.modes_for_course(self.course_id) self.assertEqual([mode1], modes) - expired_mode.expiration_date = datetime.now(pytz.UTC) + timedelta(days=1) + expiration_date = datetime.now(pytz.UTC) + timedelta(days=1) + expired_mode.expiration_date = expiration_date expired_mode.save() - expired_mode_value = Mode(u'verified', u'Verified Certificate', 0, '', 'usd') + expired_mode_value = Mode(u'verified', u'Verified Certificate', 0, '', 'usd', expiration_date.date()) modes = CourseMode.modes_for_course(self.course_id) self.assertEqual([expired_mode_value, mode1], modes) diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index e247ac08a21329913a3f0d18fa53030c7c82fc84..0993467c1727d09bed82ac400454d40a6b03917e 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -34,10 +34,19 @@ class ChooseModeView(View): @method_decorator(login_required) def get(self, request, course_id, error=None): """ Displays the course mode choice page """ - if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': + + enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id) + upgrade = request.GET.get('upgrade', False) + + # verified users do not need to register or upgrade + if enrollment_mode == 'verified': return redirect(reverse('dashboard')) - modes = CourseMode.modes_for_course_dict(course_id) + # registered users who are not trying to upgrade do not need to re-register + if enrollment_mode is not None and upgrade is False: + return redirect(reverse('dashboard')) + + modes = CourseMode.modes_for_course_dict(course_id) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(course_id, None) @@ -50,6 +59,7 @@ class ChooseModeView(View): "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, + "upgrade": upgrade, } if "verified" in modes: context["suggested_prices"] = [decimal.Decimal(x) for x in modes["verified"].suggested_prices.split(",")] @@ -70,6 +80,8 @@ class ChooseModeView(View): error_msg = _("Enrollment is closed") return self.get(request, course_id, error=error_msg) + upgrade = request.GET.get('upgrade', False) + requested_mode = self.get_requested_mode(request.POST.get("mode")) if requested_mode == "verified" and request.POST.get("honor-code"): requested_mode = "honor" @@ -106,13 +118,12 @@ class ChooseModeView(View): if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): return redirect( reverse('verify_student_verified', - kwargs={'course_id': course_id}) + kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade) ) return redirect( reverse('verify_student_show_requirements', - kwargs={'course_id': course_id}), - ) + kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade)) def get_requested_mode(self, user_choice): """ @@ -121,6 +132,7 @@ class ChooseModeView(View): """ choices = { "Select Audit": "audit", - "Select Certificate": "verified" + "Select Certificate": "verified", + "Upgrade Your Registration": "verified" } return choices.get(user_choice) diff --git a/common/djangoapps/student/tests/factories.py b/common/djangoapps/student/tests/factories.py index b2f4d95776e0eade79f2e90a1e8386c7af3045bb..107631b17b0d07736932e7a971f001f0df8959fe 100644 --- a/common/djangoapps/student/tests/factories.py +++ b/common/djangoapps/student/tests/factories.py @@ -2,6 +2,7 @@ from student.models import (User, UserProfile, Registration, CourseEnrollmentAllowed, CourseEnrollment, PendingEmailChange, UserStanding, ) +from course_modes.models import CourseMode from django.contrib.auth.models import Group from datetime import datetime from factory import DjangoModelFactory, SubFactory, PostGenerationMethodCall, post_generation, Sequence @@ -36,6 +37,16 @@ class UserProfileFactory(DjangoModelFactory): goals = u'World domination' +class CourseModeFactory(DjangoModelFactory): + FACTORY_FOR = CourseMode + + course_id = None + mode_display_name = u'Honor Code', + mode_slug = 'honor' + min_price = 0 + suggested_prices = '' + currency = 'usd' + class RegistrationFactory(DjangoModelFactory): FACTORY_FOR = Registration diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index c35ad664274df56e09f3ca0a5c9bf436cc3e5e82..315b6e928510cbe6007049cdaff1cff475c03ef2 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -8,6 +8,8 @@ import logging import json import re import unittest +from datetime import datetime, timedelta +import pytz from django.conf import settings from django.test import TestCase @@ -28,8 +30,8 @@ from textwrap import dedent from student.models import unique_id_for_user, CourseEnrollment from student.views import (process_survey_link, _cert_info, password_reset, password_reset_confirm_wrapper, - change_enrollment) -from student.tests.factories import UserFactory + change_enrollment, complete_course_mode_info) +from student.tests.factories import UserFactory, CourseModeFactory from student.tests.test_email import mock_render_to_string import shoppingcart @@ -216,6 +218,45 @@ class CourseEndingTest(TestCase): }) +@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE) +class DashboardTest(TestCase): + """ + Tests for dashboard utility functions + """ + # arbitrary constant + COURSE_SLUG = "100" + COURSE_NAME = "test_course" + COURSE_ORG = "EDX" + + def setUp(self): + self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG) + self.assertIsNotNone(self.course) + self.user = UserFactory.create(username="jack", email="jack@fake.edx.org") + CourseModeFactory.create( + course_id=self.course.id, + mode_slug='honor', + mode_display_name='Honor Code', + ) + + def test_course_mode_info(self): + verified_mode = CourseModeFactory.create( + course_id=self.course.id, + mode_slug='verified', + mode_display_name='Verified', + expiration_date=(datetime.now(pytz.UTC) + timedelta(days=1)).date() + ) + enrollment = CourseEnrollment.enroll(self.user, self.course.id) + course_mode_info = complete_course_mode_info(self.course.id, enrollment) + self.assertTrue(course_mode_info['show_upsell']) + self.assertEquals(course_mode_info['days_for_upsell'], 1) + + verified_mode.expiration_date = datetime.now(pytz.UTC) + timedelta(days=-1) + verified_mode.save() + course_mode_info = complete_course_mode_info(self.course.id, enrollment) + self.assertFalse(course_mode_info['show_upsell']) + self.assertIsNone(course_mode_info['days_for_upsell']) + + class EnrollInCourseTest(TestCase): """Tests enrolling and unenrolling in courses.""" diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 9d3d0bc63b7204aa2c6232ea7b233a29e282522c..4ebbcff59201b6cf0d20de1b196f640c0b39742e 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -267,6 +267,29 @@ def register_user(request, extra_context=None): return render_to_response('register.html', context) +def complete_course_mode_info(course_id, enrollment): + """ + We would like to compute some more information from the given course modes + and the user's current enrollment + + Returns the given information: + - whether to show the course upsell information + - numbers of days until they can't upsell anymore + """ + modes = CourseMode.modes_for_course_dict(course_id) + mode_info = {'show_upsell': False, 'days_for_upsell': None} + # we want to know if the user is already verified and if verified is an + # option + if 'verified' in modes and enrollment.mode != 'verified': + mode_info['show_upsell'] = True + # if there is an expiration date, find out how long from now it is + if modes['verified'].expiration_date: + today = datetime.datetime.now(UTC).date() + mode_info['days_for_upsell'] = (modes['verified'].expiration_date - today).days + + return mode_info + + @login_required @ensure_csrf_cookie def dashboard(request): @@ -300,6 +323,7 @@ def dashboard(request): show_courseware_links_for = frozenset(course.id for course, _enrollment in courses if has_access(request.user, course, 'load')) + course_modes = {course.id: complete_course_mode_info(course.id, enrollment) for course, enrollment in courses} cert_statuses = {course.id: cert_info(request.user, course) for course, _enrollment in courses} # only show email settings for Mongo course and when bulk email is turned on @@ -324,6 +348,7 @@ def dashboard(request): 'staff_access': staff_access, 'errored_courses': errored_courses, 'show_courseware_links_for': show_courseware_links_for, + 'all_course_modes': course_modes, 'cert_statuses': cert_statuses, 'show_email_settings_for': show_email_settings_for, } diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index 738728042b79b416797ab039245234fc898e4f11..67cc0a301d4bc305c0d4ddb108480ae72fc094b5 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -2,8 +2,16 @@ <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> -<%block name="bodyclass">register verification-process step-select-track</%block> -<%block name="title"><title>${_("Register for {} | Choose Your Track").format(course_name)}</title></%block> +<%block name="bodyclass">register verification-process step-select-track ${'is-upgrading' if upgrade else ''}</%block> +<%block name="title"> + <title> + %if upgrade: + ${_("Upgrade Your Registration for {} | Choose Your Track").format(course_name)} + %else: + ${_("Register for {} | Choose Your Track").format(course_name)} + %endif + </title> +</%block> <%block name="js_extra"> <script type="text/javascript"> @@ -48,7 +56,10 @@ $(document).ready(function() { <div class="wrapper-register-choose wrapper-content-main"> <article class="register-choose content-main"> + + %if not upgrade: <h3 class="title">${_("Select your track:")}</h3> + %endif <form class="form-register-choose" method="post" name="enrollment_mode_form" id="enrollment_mode_form"> @@ -57,9 +68,16 @@ $(document).ready(function() { <div class="wrapper-copy"> <span class="deco-ribbon"></span> <h4 class="title">${_("Certificate of Achievement (ID Verified)")}</h4> - <div class="copy"> - <p>${_("Sign up and work toward a verified Certificate of Achievement.")}</p> - </div> + + %if upgrade: + <div class="copy"> + <p>${_("Upgrade and work toward a verified Certificate of Achievement.")}</p> + </div> + %else: + <div class="copy"> + <p>${_("Sign up and work toward a verified Certificate of Achievement.")}</p> + </div> + %endif </div> <div class="field field-certificate-contribution"> @@ -115,16 +133,28 @@ $(document).ready(function() { <ul class="list-actions"> <li class="action action-select"> - <input type="submit" name="mode" value="Select Certificate" /> + %if upgrade: + <input type="submit" name="mode" value="Upgrade Your Registration" /> + %else: + <input type="submit" name="mode" value="Select Certificate" /> + %endif + </li> </ul> </div> <div class="help help-register"> <h3 class="title">${_("Verified Registration Requirements")}</h3> - <div class="copy"> - <p>${_("To register for a Verified Certificate of Achievement option, you will need a webcam, a credit or debit card, and an ID.")}</p> - </div> + + %if upgrade: + <div class="copy"> + <p>${_("To upgrade your registration and work towards a Verified Certificate of Achievement, you will need a webcam, a credit or debit card, and an ID.")}</p> + </div> + %else: + <div class="copy"> + <p>${_("To register for a Verified Certificate of Achievement option, you will need a webcam, a credit or debit card, and an ID.")}</p> + </div> + %endif <h3 class="title">${_("What is an ID Verified Certificate?")}</h3> <div class="copy"> @@ -133,25 +163,29 @@ $(document).ready(function() { </div> % endif - % if "audit" in modes: - <span class="deco-divider"> - <span class="copy">${_("or")}</span> - </span> - <div class="register-choice register-choice-audit"> - <div class="wrapper-copy"> - <h4 class="title">${_("Audit This Course")}</h4> - <div class="copy"> - <p>${_("Sign up to audit this course for free and track your own progress.")}</p> + %if not upgrade: + + % if "audit" in modes: + <span class="deco-divider"> + <span class="copy">${_("or")}</span> + </span> + <div class="register-choice register-choice-audit"> + <div class="wrapper-copy"> + <h4 class="title">${_("Audit This Course")}</h4> + <div class="copy"> + <p>${_("Sign up to audit this course for free and track your own progress.")}</p> + </div> </div> + + <ul class="list-actions"> + <li class="action action-select"> + <input type="submit" name="mode" value="Select Audit" /> + </li> + </ul> </div> + % endif - <ul class="list-actions"> - <li class="action action-select"> - <input type="submit" name="mode" value="Select Audit" /> - </li> - </ul> - </div> - % endif + %endif <input type="hidden" name="csrfmiddlewaretoken" value="${ csrf_token }"> </form> diff --git a/lms/djangoapps/courseware/features/certificates.feature b/lms/djangoapps/courseware/features/certificates.feature index 1b385b32e509bb6b36123a21f558661c691ec7a0..16eca8aabd2a477e8e0512dac63ec9cedda78704 100644 --- a/lms/djangoapps/courseware/features/certificates.feature +++ b/lms/djangoapps/courseware/features/certificates.feature @@ -35,6 +35,7 @@ Feature: LMS.Verified certificates And I navigate to my dashboard Then I see the course on my dashboard And I see that I am on the verified track + And I do not see the upsell link on my dashboard # Not easily automated # Scenario: I can re-take photos @@ -70,3 +71,24 @@ Feature: LMS.Verified certificates And the course has an honor mode When I give a reason why I cannot pay Then I should see the course on my dashboard + + Scenario: The upsell offer is on the dashboard if I am auditing. + Given I am logged in + When I select the audit track + And I navigate to my dashboard + Then I see the upsell link on my dashboard + + Scenario: I can take the upsell offer and pay for it + Given I am logged in + And I select the audit track + And I navigate to my dashboard + When I see the upsell link on my dashboard + And I select the upsell link on my dashboard + And I select the verified track for upgrade + And I submit my photos and confirm + And I am at the payment page + And I submit valid payment information + And I navigate to my dashboard + Then I see the course on my dashboard + And I see that I am on the verified track + diff --git a/lms/djangoapps/courseware/features/certificates.py b/lms/djangoapps/courseware/features/certificates.py index b8a5b958b6fc1ef36accd5806f9031403534229f..2e82afe787c42912ec52f1c5745a215d8f65beec 100644 --- a/lms/djangoapps/courseware/features/certificates.py +++ b/lms/djangoapps/courseware/features/certificates.py @@ -6,6 +6,8 @@ from lettuce.django import django_url from course_modes.models import CourseMode from nose.tools import assert_equal +UPSELL_LINK_CSS = '.message-upsell a.action-upgrade[href*="edx/999/Certificates"]' + def create_cert_course(): world.clear_courses() org = 'edx' @@ -53,7 +55,7 @@ def the_course_has_an_honor_mode(step): mode_slug='honor', mode_display_name='honor mode', min_price=0, - ) + ) assert isinstance(honor_mode, CourseMode) @@ -73,14 +75,28 @@ def select_contribution(amount=32): assert world.css_find(radio_css).selected +def click_verified_track_button(): + world.wait_for_ajax_complete() + btn_css = 'input[value="Select Certificate"]' + world.css_click(btn_css) + + +@step(u'I select the verified track for upgrade') +def select_verified_track_upgrade(step): + select_contribution(32) + world.wait_for_ajax_complete() + btn_css = 'input[value="Upgrade Your Registration"]' + world.css_click(btn_css) + # TODO: might want to change this depending on the changes for upgrade + assert world.is_css_present('section.progress') + + @step(u'I select the verified track$') def select_the_verified_track(step): create_cert_course() register() select_contribution(32) - world.wait_for_ajax_complete() - btn_css = 'input[value="Select Certificate"]' - world.css_click(btn_css) + click_verified_track_button() assert world.is_css_present('section.progress') @@ -203,6 +219,20 @@ def submitted_photos_to_verify_my_identity(step): step.given('I go to step "4"') +@step(u'I submit my photos and confirm') +def submit_photos_and_confirm(step): + step.given('I go to step "1"') + step.given('I capture my "face" photo') + step.given('I approve my "face" photo') + step.given('I go to step "2"') + step.given('I capture my "photo_id" photo') + step.given('I approve my "photo_id" photo') + step.given('I go to step "3"') + step.given('I select a contribution amount') + step.given('I confirm that the details match') + step.given('I go to step "4"') + + @step(u'I see that my payment was successful') def see_that_my_payment_was_successful(step): title = world.css_find('div.wrapper-content-main h3.title') @@ -221,6 +251,27 @@ def see_the_course_on_my_dashboard(step): assert world.is_css_present(course_link_css) +@step(u'I see the upsell link on my dashboard') +def see_upsell_link_on_my_dashboard(step): + course_link_css = UPSELL_LINK_CSS + assert world.is_css_present(course_link_css) + + +@step(u'I do not see the upsell link on my dashboard') +def see_upsell_link_on_my_dashboard(step): + course_link_css = UPSELL_LINK_CSS + assert not world.is_css_present(course_link_css) + + +@step(u'I select the upsell link on my dashboard') +def see_upsell_link_on_my_dashboard(step): + # expand the upsell section + world.css_click('.message-upsell') + course_link_css = UPSELL_LINK_CSS + # click the actual link + world.css_click(course_link_css) + + @step(u'I see that I am on the verified track') def see_that_i_am_on_the_verified_track(step): id_verified_css = 'li.course-item article.course.verified' diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index b220ff6a97d1db94c5c36332a9001bbb20d3cdda..8fafc26834333051f1d2f82d5eb02e351975d076 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -44,12 +44,15 @@ class VerifyView(View): before proceeding to payment """ + upgrade = request.GET.get('upgrade', False) + # If the user has already been verified within the given time period, # redirect straight to the payment -- no need to verify again. if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): return redirect( reverse('verify_student_verified', - kwargs={'course_id': course_id})) + kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade) + ) elif CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) else: @@ -85,6 +88,7 @@ class VerifyView(View): "currency": verify_mode.currency.upper(), "chosen_price": chosen_price, "min_price": verify_mode.min_price, + "upgrade": upgrade, } return render_to_response('verify_student/photo_verification.html', context) @@ -100,6 +104,7 @@ class VerifiedView(View): """ Handle the case where we have a get request """ + upgrade = request.GET.get('upgrade', False) if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) verify_mode = CourseMode.mode_for_course(course_id, "verified") @@ -117,6 +122,7 @@ class VerifiedView(View): "purchase_endpoint": get_purchase_endpoint(), "currency": verify_mode.currency.upper(), "chosen_price": chosen_price, + "upgrade": upgrade, } return render_to_response('verify_student/verified.html', context) @@ -250,6 +256,7 @@ def show_requirements(request, course_id): if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) + upgrade = request.GET.get('upgrade', False) course = course_from_id(course_id) context = { "course_id": course_id, @@ -257,5 +264,6 @@ def show_requirements(request, course_id): "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "is_not_active": not request.user.is_active, + "upgrade": upgrade, } return render_to_response("verify_student/show_requirements.html", context) diff --git a/lms/static/sass/base/_extends.scss b/lms/static/sass/base/_extends.scss index ebd5ae806ec499677cc2545d2272a259c2ca0881..abfdaa0d5c6360ecd6221d973d20f477f2e3dfde 100644 --- a/lms/static/sass/base/_extends.scss +++ b/lms/static/sass/base/_extends.scss @@ -100,3 +100,16 @@ // outline: thin dotted !important; } } + +// removes list styling/spacing when using uls, ols for navigation and less content-centric cases +%ui-no-list { + list-style: none; + margin: 0; + padding: 0; + text-indent: 0; + + li { + margin: 0; + padding: 0; + } +} diff --git a/lms/static/sass/elements/_typography.scss b/lms/static/sass/elements/_typography.scss index fc9acabc89593ad84920f7e1ace0341455237204..68a3cd50df1388f910ac8bc1892894b818c73de5 100644 --- a/lms/static/sass/elements/_typography.scss +++ b/lms/static/sass/elements/_typography.scss @@ -282,4 +282,5 @@ border-radius: ($baseline/5); padding: ($baseline/2) $baseline; text-transform: uppercase; + letter-spacing: 0.1rem; } diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index ad8919f69a1b07742ef405076ea7583a475df323..47582dd5382404edf0fca2eec26c39e0b9ef786c 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -227,6 +227,8 @@ } } + // ==================== + // course listings .my-courses { float: left; @@ -272,6 +274,8 @@ } } + // ==================== + // UI: course list .listing-courses { @extend %ui-no-list; @@ -289,6 +293,8 @@ } } + // ==================== + // UI: individual course item .course { @include box-sizing(box); @@ -416,25 +422,27 @@ } } + // ==================== + // STATE: course mode - verified &.verified { @extend %ui-depth2; - margin-top: ($baseline*2.5); - border-top: 1px solid $verified-color-lvl3; - padding-top: ($baseline*1.25); - background: $white; + position: relative; - // FIXME: bad, but needed selector! - .info > hgroup .date-block { - top: ($baseline*1.25); + .cover { + border-radius: ($baseline/10); + border: 1px solid $verified-color-lvl3; + border-bottom: 4px solid $verified-color-lvl3; + padding: ($baseline/10); } // course enrollment status message .sts-enrollment { display: inline-block; position: absolute; - top: -28px; - right: ($baseline/2); + top: 105px; + left: 55px; + bottom: ($baseline/2); text-align: center; .label { @@ -454,53 +462,176 @@ @extend %copy-badge; border-radius: 0; padding: ($baseline/4) ($baseline/2) ($baseline/4) $baseline; - color: $white; background: $verified-color-lvl3; + color: $white; } } } - } - .message-status { + // ==================== + + // UI: message + .message { @include clearfix; border-radius: 3px; display: none; z-index: 10; - margin: 20px 0 10px; - padding: 15px 20px; + margin: $baseline 0 ($baseline/2) 0; + padding: ($baseline*0.75) $baseline; font-family: $sans-serif; background: tint($yellow,70%); border: 1px solid #ccc; - .message-copy { + // STATE: shown + &.is-shown { + display: block; + } + + a { font-family: $sans-serif; - font-size: 13px; - margin: 0; + } + + strong { + font-weight: 700; a { - font-family: $sans-serif; + font-weight: 700; } + } - .grade-value { - font-size: 1.2rem; - font-weight: bold; + .actions { + @include clearfix; + list-style: none; + margin: 0; + padding: 0; + } + + .message-title, + .message-copy .title { + @extend %t-title5; + @extend %t-weight4; + margin-bottom: ($baseline/4); + } + + .message-copy, + .message-copy .copy { + @extend %t-copy-sub1; + margin: 0; + } + + // CASE: expandable + &.is-expandable { + + .wrapper-tip { + + .message-title, .message-copy { + @include transition(color 0.25s ease-in-out 0); + margin-bottom: 0; + } + + // STATE: hover + &:hover { + cursor: pointer; + + .message-title, .message-copy { + color: $link-color; + } + } } - strong { - font-weight: 700; + .wrapper-extended { + @include transition(opacity 0.25s ease-in-out 0); + display: none; + opacity: 0.0; + } - a { - font-weight: 700; + // STATE: is expanded + &.is-expanded { + + .wrapper-extended { + display: block; + opacity: 1.0; } } } + } - .actions { - @include clearfix; - list-style: none; + // TYPE: upsell + .message-upsell { + + .wrapper-tip { + @include clearfix(); + + .message-title { + float: left; + } + + .message-copy { + float: right; + } + } + + .wrapper-extended { + padding: ($baseline/2) 0; + + .message-copy { + margin-bottom: $baseline; + } + } + + .action-upgrade { + @extend %btn-primary-green; + @include clearfix(); + position: relative; + left: ($baseline/2); + padding: 8px $baseline 8px ($baseline*2); + + .deco-graphic { + position: absolute; + top: -($baseline/4); + left: -($baseline*0.75); + width: ($baseline*2); + } + + span { + color: $white; // nasty but needed override for poor <span> styling + } + + .copy, .copy-sub { + display: inline-block; + vertical-align: middle; + } + + .copy { + @extend %t-action3; + @extend %t-weight4; + margin-right: $baseline; + } + + .copy-sub { + @extend %t-action4; + opacity: 0.875; + } + } + } + + // TYPE: status + .message-status { + background: tint($yellow,70%); + border-color: #ccc; + + .message-copy { + @extend %t-copy-sub1; margin: 0; - padding: 0; + + .grade-value { + font-size: 1.2rem; + font-weight: bold; + } + } + + .actions { .action { float: left; @@ -589,10 +720,6 @@ } } - &.is-shown { - display: block; - } - &.course-status-processing { } @@ -614,7 +741,6 @@ } } - a.unenroll { float: right; display: block; diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 589c45f7d36f30a32ceeb73fdd7668ec6903ff14..43950d6f41bef054e9a810ceb7f46ac4ed7bd3e2 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -1,6 +1,87 @@ // lms - views - verification flow // ==================== +// MISC: extends - type +// application: canned headings +%hd-lv1 { + @extend %t-title1; + @extend %t-weight1; + color: $m-gray-d4; + margin: 0 0 ($baseline*2) 0; +} + +%hd-lv2 { + @extend %t-title4; + @extend %t-weight1; + margin: 0 0 ($baseline*0.75) 0; + border-bottom: 1px solid $m-gray-l4; + padding-bottom: ($baseline/2); + color: $m-gray-d4; +} + +%hd-lv3 { + @extend %t-title6; + @extend %t-weight4; + margin: 0 0 ($baseline/4) 0; + color: $m-gray-d4; +} + +%hd-lv4 { + @extend %t-title6; + @extend %t-weight2; + margin: 0 0 $baseline 0; + color: $m-gray-d4; +} + +%hd-lv5 { + @extend %t-title7; + @extend %t-weight4; + margin: 0 0 ($baseline/4) 0; + color: $m-gray-d4; +} + +// application: canned copy +%copy-base { + @extend %t-copy-base; + color: $m-gray-d2; +} + +%copy-lead1 { + @extend %t-copy-lead2; + color: $m-gray; +} + +%copy-detail { + @extend %t-copy-sub1; + @extend %t-weight3; + color: $m-gray-d1; +} + +%copy-metadata { + @extend %t-copy-sub2; + color: $m-gray-d1; + + + %copy-metadata-value { + @extend %t-weight2; + } + + %copy-metadata-value { + @extend %t-weight4; + } +} + +// application: canned links +%copy-link { + border-bottom: 1px dotted transparent; + + &:hover, &:active { + border-color: $link-color-d1; + } +} + +// ==================== + // MISC: extends - button %btn-verify-primary { @extend %btn-primary-green; @@ -72,7 +153,7 @@ // ==================== // VIEW: all verification steps -.register.verification-process { +.verification-process { // reset: box-sizing (making things so right its scary) * { @@ -179,12 +260,16 @@ // elements - controls .action-primary { @extend %btn-primary-blue; - border: none; + // needed for override due to .register a:link styling + border: 0 !important; + color: $white !important; } .action-confirm { @extend %btn-verify-primary; - border: none; + // needed for override due to .register a:link styling + border: 0 !important; + color: $white !important; } // ==================== @@ -382,17 +467,19 @@ margin-right: ($baseline/4); opacity: 0.80; color: $white; + letter-spacing: 0.1rem; } } } .sts-label { @extend %t-title7; + @extend %t-weight4; display: block; margin-bottom: ($baseline/2); border-bottom: ($baseline/10) solid $m-gray-l4; padding-bottom: ($baseline/2); - color: $m-gray; + color: $m-gray-d1; } .sts-course { @@ -1816,3 +1903,11 @@ width: 32% !important; } } + +// STATE: upgrading registration type +.register.is-upgrading { + + .form-register-choose { + margin-top: ($baseline*2) !important; + } +} diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 683422133fb537cd04ea729f2c344cd4cb16e396..3310451c941831e2357ba7f2fc82f7cb67383b7d 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -14,6 +14,14 @@ <script type="text/javascript"> (function() { + $('.message.is-expandable .wrapper-tip').bind('click', toggleExpandMessage); + + function toggleExpandMessage(e) { + (e).preventDefault(); + + $(this).closest('.message.is-expandable').toggleClass('is-expanded'); + } + $(".email-settings").click(function(event) { $("#email_settings_course_id").val( $(event.target).data("course-id") ); $("#email_settings_course_number").text( $(event.target).data("course-number") ); @@ -179,7 +187,8 @@ <% show_courseware_link = (course.id in show_courseware_links_for) %> <% cert_status = cert_statuses.get(course.id) %> <% show_email_settings = (course.id in show_email_settings_for) %> - <%include file='dashboard/dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings" /> + <% course_mode_info = all_course_modes.get(course.id) %> + <%include file='dashboard/dashboard_course_listing.html' args="course=course, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info" /> % endfor </ul> diff --git a/lms/templates/dashboard/dashboard_course_listing.html b/lms/templates/dashboard/dashboard_course_listing.html index 3442cb93aa908f3bfcb90fdf6faca7129937f3ab..0a7925e93bb45ad6ba928d7f1282a24ecc8427f3 100644 --- a/lms/templates/dashboard/dashboard_course_listing.html +++ b/lms/templates/dashboard/dashboard_course_listing.html @@ -1,4 +1,4 @@ -<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings" /> +<%page args="course, enrollment, show_courseware_link, cert_status, show_email_settings, course_mode_info" /> <%! from django.utils.translation import ugettext as _ %> <%! @@ -29,11 +29,11 @@ % endif % if enrollment.mode == "verified": - <span class="sts-enrollment"> - <span class="label">${_("Enrolled as: ")}</span> - <img class="deco-graphic" src="${static.url('images/vcert-ribbon-s.png')}" alt="ID Verified Ribbon/Badge"> - <span class="sts-enrollment-value">${_("ID Verified")}</span> - </span> + <span class="sts-enrollment"> + <span class="label">${_("Enrolled as: ")}</span> + <img class="deco-graphic" src="${static.url('images/vcert-ribbon-s.png')}" alt="ID Verified Ribbon/Badge" /> + <span class="sts-enrollment-value">${_("ID Verified")}</span> + </span> % endif <section class="info"> @@ -95,12 +95,12 @@ <li class="action"> <a class="btn" href="${cert_status['download_url']}" title="${_('This link will open/download a PDF document')}"> - Download Your PDF Certificate</a></li> + ${_("Download Your PDF Certificate")}</a></li> % endif % if cert_status['show_survey_button']: <li class="action"><a class="cta" href="${cert_status['survey_url']}"> - ${_('Complete our course feedback survey')}</a></li> + ${_("Complete our course feedback survey")}</a></li> % endif </ul> % endif @@ -108,6 +108,31 @@ % endif + %if course_mode_info['show_upsell']: + <div class="message message-upsell has-actions is-expandable is-shown"> + + <div class="wrapper-tip"> + <h4 class="message-title">${_("Challenge Yourself!")}</h4> + <p class="message-copy">${_("Take this course as an ID-verified student.")}</p> + </div> + + <div class="wrapper-extended"> + <p class="message-copy">${_("You can still sign up for an ID verified certificate for this course. If you plan to complete the whole course, it is a great way to recognize your achievement. {a_start}Learn more about verified certificates{a_end}.").format(a_start='<a href="{}">'.format(marketing_link('WHAT_IS_VERIFIED_CERT')), a_end="</a>")}</p> + + <ul class="actions message-actions"> + <li class="action-item"> + <a class="action action-upgrade" href="${reverse('course_modes_choose', kwargs={'course_id': course.id})}?upgrade=True"> + <img class="deco-graphic" src="${static.url('images/vcert-ribbon-s.png')}" alt="ID Verified Ribbon/Badge"> + <span class="wrapper-copy"> + <span class="copy">${_("Upgrade to Verified Track")}</span> + </span> + </a> + </li> + </ul> + </div> + </div> + %endif + % if show_courseware_link: % if course.has_ended(): <a href="${course_target}" class="enter-course archived">${_('View Archived Course')}</a> @@ -116,8 +141,6 @@ % endif % endif - - <a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">${_('Unregister')}</a> % if show_email_settings: diff --git a/lms/templates/verify_student/_verification_header.html b/lms/templates/verify_student/_verification_header.html index 4870a59c4937259f2646f3d3caa0153c534ea11e..80a1e939b0fe849ba5a030ab686c6ceff9a24fd9 100644 --- a/lms/templates/verify_student/_verification_header.html +++ b/lms/templates/verify_student/_verification_header.html @@ -2,7 +2,11 @@ <header class="page-header"> <h2 class="title"> - <span class="sts-label">${_("You are registering for")}</span> + %if upgrade: + <span class="sts-label">${_("You are upgrading your registration for")}</span> + %else: + <span class="sts-label">${_("You are registering for")}</span> + %endif <span class="wrapper-sts"> <span class="sts-course"> @@ -13,7 +17,11 @@ <span class="sts-track"> <span class="sts-track-value"> - <span class="context">${_("Registering as: ")}</span> ${_("ID Verified")} + %if upgrade: + <span class="context">${_("Upgrading to:")}</span> ${_("ID Verified")} + %else: + <span class="context">${_("Registering as: ")}</span> ${_("ID Verified")} + %endif </span> </span> </span> diff --git a/lms/templates/verify_student/_verification_support.html b/lms/templates/verify_student/_verification_support.html index 0503c3bf7d3f89130726f202f0a7a7788d6e6116..260ba7bae38058001f77d43f46617339291f4c1f 100644 --- a/lms/templates/verify_student/_verification_support.html +++ b/lms/templates/verify_student/_verification_support.html @@ -11,14 +11,21 @@ </li> <li class="help-item help-item-coldfeet"> - <h3 class="title">${_("Change your mind?")}</h3> - <div class="copy"> - <p>${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='<a rel="external" href="/course_modes/choose/' + course_id + '">', a_end="</a>")}</p> - </div> + %if upgrade: + <h3 class="title">${_("Change your mind?")}</h3> + <div class="copy"> + <p>${_("You can always continue to audit the course without verifying.")}</p> + </div> + %else: + <h3 class="title">${_("Change your mind?")}</h3> + <div class="copy"> + <p>${_("You can always {a_start} audit the course for free {a_end} without verifying.").format(a_start='<a rel="external" href="/course_modes/choose/' + course_id + '">', a_end="</a>")}</p> + </div> + %endif </li> <li class="help-item help-item-technical"> - <h3 class="title">${_("Having Technical Trouble?")}</h3> + <h3 class="title">${_("Technical Requirements")}</h3> <div class="copy"> <p>${_("Please make sure your browser is updated to the {strong_start}{a_start}most recent version possible{a_end}{strong_end}. Also, please make sure your {strong_start}web cam is plugged in, turned on, and allowed to function in your web browser (commonly adjustable in your browser settings).{strong_end}").format(a_start='<a rel="external" href="http://browsehappy.com/">', a_end="</a>", strong_start="<strong>", strong_end="</strong>")}</p> </div> diff --git a/lms/templates/verify_student/photo_verification.html b/lms/templates/verify_student/photo_verification.html index 537c26ff8412695d824f49da57de3f0b73d1a329..92b976ce51bb1610efba344f5fbfb4adbecb1d41 100644 --- a/lms/templates/verify_student/photo_verification.html +++ b/lms/templates/verify_student/photo_verification.html @@ -3,8 +3,16 @@ <%inherit file="../main.html" /> <%namespace name='static' file='/static_content.html'/> -<%block name="bodyclass">register verification-process step-photos</%block> -<%block name="title"><title>${_("Register for {} | Verification").format(course_name)}</title></%block> +<%block name="bodyclass">register verification-process step-photos ${'is-upgrading' if upgrade else ''}</%block> +<%block name="title"> + <title> + %if upgrade: + ${_("Upgrade Your Registration for {} | Verification").format(course_name)} + %else: + ${_("Register for {} | Verification").format(course_name)} + %endif + </title> +</%block> <%block name="js_extra"> <script src="${static.url('js/vendor/responsive-carousel/responsive-carousel.js')}"></script> @@ -172,7 +180,12 @@ <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> - <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="/course_modes/choose/' + course_id + '">', a_end="</a>")}</dd> + + %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="/course_modes/choose/' + course_id + '">', a_end="</a>")}</dd> + %endif </dl> </div> </div> diff --git a/lms/templates/verify_student/show_requirements.html b/lms/templates/verify_student/show_requirements.html index 432a13fc62a9ddf3a93f01391abe691f24384009..3280fec89a29128cf3a5b869a9b0e3bb81634cf2 100644 --- a/lms/templates/verify_student/show_requirements.html +++ b/lms/templates/verify_student/show_requirements.html @@ -1,8 +1,16 @@ <%! from django.utils.translation import ugettext as _ %> <%! from django.core.urlresolvers import reverse %> <%inherit file="../main.html" /> -<%block name="bodyclass">register verification-process step-requirements</%block> -<%block name="title"><title>${_("Register for {}").format(course_name)}</title></%block> +<%block name="bodyclass">register verification-process step-requirements ${'is-upgrading' if upgrade else ''}</%block> +<%block name="title"> + <title> + %if upgrade: + ${_("Upgrade Your Registration for {}").format(course_name)} + %else: + ${_("Register for {}").format(course_name)} + %endif + </title> +</%block> <%block name="content"> %if is_not_active: @@ -71,11 +79,19 @@ <div class="wrapper-content-main"> <article class="content-main"> - <h3 class="title">${_("What You Will Need to Register")}</h3> + %if upgrade: + <h3 class="title">${_("What You Will Need to Upgrade")}</h3> + + <div class="instruction"> + <p>${_("There are three things you will need to upgrade to being an ID verified student:")}</p> + </div> + %else: + <h3 class="title">${_("What You Will Need to Register")}</h3> - <div class="instruction"> - <p>${_("There are three things you will need to register as an ID verified student:")}</p> - </div> + <div class="instruction"> + <p>${_("There are three things you will need to register as an ID verified student:")}</p> + </div> + %endif <ul class="list-reqs ${"account-not-activated" if is_not_active else ""}"> %if is_not_active: @@ -149,11 +165,16 @@ </ul> <nav class="nav-wizard ${"is-not-ready" if is_not_active else "is-ready"}"> - <span class="help help-inline">${_("Missing something? You can always {a_start} audit this course instead {a_end}").format(a_start='<a href="/course_modes/choose/' + course_id + '">', a_end="</a>")}</span> + + %if upgrade: + <span class="help help-inline">${_("Missing something? You can always continue to audit this course instead.")}</span> + %else: + <span class="help help-inline">${_("Missing something? You can always {a_start} audit this course instead {a_end}").format(a_start='<a href="/course_modes/choose/' + course_id + '">', a_end="</a>")}</span> + %endif <ol class="wizard-steps"> <li class="wizard-step"> - <a class="next action-primary ${"disabled" if is_not_active else ""}" id="face_next_button" href="${reverse('verify_student_verify', kwargs={'course_id': course_id})}">${_("Go to Step 1: Take my Photo")}</a> + <a class="next action-primary ${"disabled" if is_not_active else ""}" id="face_next_button" href="${reverse('verify_student_verify', kwargs={'course_id': course_id})}?upgrade=${upgrade}">${_("Go to Step 1: Take my Photo")}</a> </li> </ol> </nav>