diff --git a/common/djangoapps/course_modes/admin.py b/common/djangoapps/course_modes/admin.py index 71231d89f2..7645ccb3a1 100644 --- a/common/djangoapps/course_modes/admin.py +++ b/common/djangoapps/course_modes/admin.py @@ -65,8 +65,9 @@ class CourseModeAdmin(admin.ModelAdmin): search_fields = ('course_id',) list_display = ( 'id', 'course_id', 'mode_slug', 'mode_display_name', 'min_price', - 'suggested_prices', 'currency', 'expiration_date', 'expiration_datetime_custom', 'sku' + 'currency', 'expiration_date', 'expiration_datetime_custom', 'sku' ) + exclude = ('suggested_prices',) def expiration_datetime_custom(self, obj): """adding custom column to show the expiry_datetime""" diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index e05bdd2dba..70d6963b67 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -553,8 +553,8 @@ class CourseMode(models.Model): ) def __unicode__(self): - return u"{} : {}, min={}, prices={}".format( - self.course_id.to_deprecated_string(), self.mode_slug, self.min_price, self.suggested_prices + return u"{} : {}, min={}".format( + self.course_id, self.mode_slug, self.min_price ) diff --git a/common/templates/course_modes/_contribution.html b/common/templates/course_modes/_contribution.html deleted file mode 100644 index ecab7280c5..0000000000 --- a/common/templates/course_modes/_contribution.html +++ /dev/null @@ -1,32 +0,0 @@ - diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index 3482313e66..7d849f0412 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -62,13 +62,9 @@
- <%include file="/verify_student/_verification_header.html" args="course_name=course_name" /> -
- % if not upgrade: -

${_("Now choose your course track:")}

- % endif + <%include file="/verify_student/_verification_header.html" args="course_name=course_name" />
% if "verified" in modes: @@ -77,39 +73,31 @@

${_("Pursue a Verified Certificate")}

- % if upgrade: -
-

${_("Plan to use your completed coursework for job applications, career advancement, or school applications? Upgrade to work toward a Verified Certificate of Achievement to document your accomplishment. A minimum fee applies.")}

-
- % else: -
-

${_("Plan to use your completed coursework for job applications, career advancement, or school applications? Then work toward a Verified Certificate of Achievement to document your accomplishment. A minimum fee applies.")}

-
- % endif -
- -
-
${_("Select your contribution for this course (min. $")} ${min_price} ${currency}${_("):")}
- - % if error: -
-
-

${error}

+
+

${_("Highlight you new knowledge and skills with a verified certificate. Use this valuable credential to improve your job prospects and advance your career, or highlight your certificate in school applications.")}

+

+

+
+

+ ${_("Benefits of a verified Certificate")} +

+
    +
  • ${_("Official")}: ${_("Receive an instructor-signed certificate with the institution's logo")}
  • +
  • ${_("Easily sharable")}: ${_("Add the certificate to your CV or resume, or post it directly on LinkedIn")}
  • +
  • ${_("Motivating")}: ${_("Give yourself an additional incentive to complete the course")}
  • +
+
+
+
    +
  • + + +
  • +
+
-
- % endif - - <%include file="_contribution.html" args="suggested_prices=suggested_prices, currency=currency, chosen_price=chosen_price, min_price=min_price"/> - -
    -
  • - % if upgrade: - - % else: - - % endif -
  • -
+

+
% endif @@ -131,7 +119,7 @@
diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py index 5849755de0..a51b97dba7 100644 --- a/lms/djangoapps/verify_student/models.py +++ b/lms/djangoapps/verify_student/models.py @@ -215,19 +215,29 @@ class PhotoVerification(StatusModel): ).exists() @classmethod - def user_has_valid_or_pending(cls, user, earliest_allowed_date=None, window=None, queryset=None): + def verification_valid_or_pending(cls, user, earliest_allowed_date=None, window=None, queryset=None): """ - Return whether the user has a complete verification attempt that is or - *might* be good. This means that it's approved, been submitted, or would - have been submitted but had an non-user error when it was being - submitted. It's basically any situation in which the user has signed off - on the contents of the attempt, and we have not yet received a denial. + Check whether the user has a complete verification attempt that is + or *might* be good. This means that it's approved, been submitted, + or would have been submitted but had an non-user error when it was + being submitted. + It's basically any situation in which the user has signed off on + the contents of the attempt, and we have not yet received a denial. - If window=None, this will check for the user's *initial* verification. If - window is anything else, this will check for the reverification associated - with that window. + Arguments: + user: + earliest_allowed_date: earliest allowed date given in the + settings + window: If window=None, this will check for the user's + *initial* verification. + If window is anything else, this will check for the + reverification associated with that window. + queryset: If a queryset is provided, that will be used instead + of hitting the database. - If a queryset is provided, that will be used instead of hitting the database. + Returns: + queryset: queryset of 'PhotoVerification' sorted by 'created_at' in + descending order. """ valid_statuses = ['submitted', 'approved'] if not window: @@ -242,7 +252,17 @@ class PhotoVerification(StatusModel): or cls._earliest_allowed_date() ), window=window, - ).exists() + ).order_by('-created_at') + + @classmethod + def user_has_valid_or_pending(cls, user, earliest_allowed_date=None, window=None, queryset=None): + """ + Check whether the user has an active or pending verification attempt + + Returns: + bool: True or False according to existence of valid verifications + """ + return cls.verification_valid_or_pending(user, earliest_allowed_date, window, queryset).exists() @classmethod def active_for_user(cls, user, window=None): diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 39dca7c193..86934d9aed 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -376,6 +376,9 @@ class PayAndVerifyView(View): # so we can fire an analytics event upon payment. request.session['attempting_upgrade'] = (message == self.UPGRADE_MSG) + # Determine the photo verification status + verification_good_until = self._verification_valid_until(request.user) + # Render the top-level page context = { 'contribution_amount': contribution_amount, @@ -396,6 +399,8 @@ class PayAndVerifyView(View): get_default_time_display(unexpired_paid_course_mode.expiration_datetime) if unexpired_paid_course_mode.expiration_datetime else "" ), + 'already_verified': already_verified, + 'verification_good_until': verification_good_until, } return render_to_response("verify_student/pay_and_verify.html", context) @@ -579,6 +584,26 @@ class PayAndVerifyView(View): return all_requirements + def _verification_valid_until(self, user, date_format="%m/%d/%Y"): + """ + Check whether the user has a valid or pending verification. + + Arguments: + user: + date_format: optional parameter for formatting datetime + object to string in response + + Returns: + datetime object in string format + """ + photo_verifications = SoftwareSecurePhotoVerification.verification_valid_or_pending(user) + # return 'expiration_datetime' of latest photo verification if found, + # otherwise implicitly return '' + if photo_verifications: + return photo_verifications[0].expiration_datetime.strftime(date_format) + + return '' + def _check_already_verified(self, user): """Check whether the user has a valid or pending verification. diff --git a/lms/static/js/spec/verify_student/make_payment_step_view_spec.js b/lms/static/js/spec/verify_student/make_payment_step_view_spec.js index 6da5b85d34..fa90b016e3 100644 --- a/lms/static/js/spec/verify_student/make_payment_step_view_spec.js +++ b/lms/static/js/spec/verify_student/make_payment_step_view_spec.js @@ -20,10 +20,10 @@ define([ var STEP_DATA = { minPrice: "12", - suggestedPrices: ["34.56", "78.90"], currency: "usd", purchaseEndpoint: PAYMENT_URL, - courseKey: "edx/test/test" + courseKey: "edx/test/test", + courseModeSlug: 'verified' }; var SERVER_ERROR_MSG = "An error occurred!"; @@ -41,40 +41,12 @@ define([ return view; }; - var expectPriceOptions = function( prices ) { - var sel; - _.each( prices, function( price ) { - sel = _.sprintf( 'input[name="contribution"][value="%s"]', price ); - expect( $( sel ).length > 0 ).toBe( true ); - }); - }; - var expectPriceSelected = function( price ) { - var sel = $( _.sprintf( 'input[name="contribution"][value="%s"]', price ) ); + var sel = $( 'input[name="contribution"]' ); - // If the option is available, it should be selected - if ( sel.length > 0 ) { - expect( sel.prop( 'checked' ) ).toBe( true ); - } else { - // Otherwise, the text box amount should be filled in - expect( $( '#contribution-other' ).prop( 'checked' ) ).toBe( true ); - expect( $( '#contribution-other-amt' ).val() ).toEqual( price ); - } - }; - - var choosePriceOption = function( price ) { - var sel = _.sprintf( 'input[name="contribution"][value="%s"]', price ); - $( sel ).trigger( 'click' ); - }; - - var enterPrice = function( price ) { - $( '#contribution-other' ).trigger( 'click' ); - $( '#contribution-other-amt' ).val( price ); - }; - - var expectSinglePriceDisplayed = function( price ) { - var displayedPrice = $( '.contribution-option .label-value' ).text(); - expect( displayedPrice ).toEqual( price ); + // check that contribution value is same as price given + expect( sel.length ).toEqual(1); + expect( sel.val() ).toEqual(price); }; var expectPaymentButtonEnabled = function( isEnabled ) { @@ -126,11 +98,6 @@ define([ expect(form.attr('action')).toEqual(PAYMENT_URL); }; - var expectErrorDisplayed = function( errorTitle ) { - var actualTitle = $( '#error h3.title' ).text(); - expect( actualTitle ).toEqual( errorTitle ); - }; - beforeEach(function() { window.analytics = jasmine.createSpyObj('analytics', ['track', 'page', 'trackLink']); @@ -138,29 +105,25 @@ define([ TemplateHelpers.installTemplate( 'templates/verify_student/make_payment_step' ); }); - it( 'allows users to choose a suggested price', function() { + it( 'shows users only minimum price', function() { var view = createView({}), requests = AjaxHelpers.requests(this); - expectPriceOptions( STEP_DATA.suggestedPrices ); - expectPaymentButtonEnabled( false ); - - choosePriceOption( STEP_DATA.suggestedPrices[1] ); + expectPriceSelected( STEP_DATA.minPrice ); expectPaymentButtonEnabled( true ); - goToPayment( requests, { - amount: STEP_DATA.suggestedPrices[1], + amount: STEP_DATA.minPrice, courseId: STEP_DATA.courseKey, succeeds: true }); expectPaymentSubmitted( view, PAYMENT_PARAMS ); }); - it( 'allows users to pay the minimum price if no suggested prices are given', function() { - var view = createView({ suggestedPrices: [] }), + it( 'by default minimum price is selected if no suggested prices are given', function() { + var view = createView(), requests = AjaxHelpers.requests( this ); - expectSinglePriceDisplayed( STEP_DATA.minPrice ); + expectPriceSelected( STEP_DATA.minPrice); expectPaymentButtonEnabled( true ); goToPayment( requests, { @@ -171,49 +134,14 @@ define([ expectPaymentSubmitted( view, PAYMENT_PARAMS ); }); - it( 'allows the user to enter a contribution amount', function() { - var view = createView({}), - requests = AjaxHelpers.requests( this ); - - enterPrice( "67.89" ); - expectPaymentButtonEnabled( true ); - goToPayment( requests, { - amount: "67.89", - courseId: STEP_DATA.courseKey, - succeeds: true - }); - expectPaymentSubmitted( view, PAYMENT_PARAMS ); - }); - - it( 'selects in the contribution amount if provided', function() { - // Pre-select one of the suggested prices - createView({ - contributionAmount: STEP_DATA.suggestedPrices[1] - }); - - // Expect that the price is selected - expectPriceSelected( STEP_DATA.suggestedPrices[1]); - }); - - it( 'fills in the contribution amount if provided', function() { + it( 'min price is always selected even if contribution amount is provided', function() { // Pre-select a price NOT in the suggestions createView({ contributionAmount: '99.99' }); // Expect that the price is filled in - expectPriceSelected( '99.99' ); - }); - - it( 'ignores the contribution pre-selected if no suggested prices are given', function() { - // No suggested prices, but a contribution is set - createView({ - suggestedPrices: [], - contributionAmount: '99.99' - }); - - // Expect that the single price is displayed - expectSinglePriceDisplayed( STEP_DATA.minPrice ); + expectPriceSelected( STEP_DATA.minPrice ); }); it( 'disables payment for inactive users', function() { @@ -225,9 +153,8 @@ define([ var requests = AjaxHelpers.requests( this ), view = createView({}); - choosePriceOption( STEP_DATA.suggestedPrices[0] ); goToPayment( requests, { - amount: STEP_DATA.suggestedPrices[0], + amount: STEP_DATA.minPrice, courseId: STEP_DATA.courseKey, succeeds: false }); diff --git a/lms/static/js/verify_student/pay_and_verify.js b/lms/static/js/verify_student/pay_and_verify.js index d7c2eb943c..c7a8a8b043 100644 --- a/lms/static/js/verify_student/pay_and_verify.js +++ b/lms/static/js/verify_student/pay_and_verify.js @@ -60,7 +60,10 @@ var edx = edx || {}; ), currency: el.data('course-mode-currency'), purchaseEndpoint: el.data('purchase-endpoint'), - verificationDeadline: el.data('verification-deadline') + verificationDeadline: el.data('verification-deadline'), + courseModeSlug: el.data('course-mode-slug'), + alreadyVerified: el.data('already-verified'), + verificationGoodUntil: el.data('verification-good-until') }, 'payment-confirmation-step': { courseKey: el.data('course-key'), diff --git a/lms/static/js/verify_student/views/make_payment_step_view.js b/lms/static/js/verify_student/views/make_payment_step_view.js index ad519a9cc1..0dd80092b4 100644 --- a/lms/static/js/verify_student/views/make_payment_step_view.js +++ b/lms/static/js/verify_student/views/make_payment_step_view.js @@ -21,7 +21,10 @@ var edx = edx || {}; courseName: '', requirements: {}, hasVisibleReqs: false, - platformName: '' + platformName: '', + alreadyVerified: false, + courseModeSlug: 'honor', + verificationGoodUntil: '' }; }, @@ -35,15 +38,6 @@ var edx = edx || {}; // Track a virtual pageview, for easy funnel reconstruction. window.analytics.page( 'payment', this.templateName ); - // Set the payment button to disabled by default - this.setPaymentEnabled( false ); - - // Update the contribution amount with the amount the user - // selected in a previous screen. - if ( templateContext.contributionAmount ) { - this.selectPaymentAmount( templateContext.contributionAmount ); - } - // The contribution section is hidden by default // Display it if the user hasn't already selected an amount // or is upgrading. diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index 9549b2093a..c530b9a2fa 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -1231,6 +1231,17 @@ @extend %t-copy-base; } + .wrapper-copy-inline { + @extend %t-copy-base; + display: inline-block; + width: 100%; + } + + .copy-inline { + @extend %t-copy-base; + display: inline-block; + } + .action-select { @include fill-parent; @@ -2415,6 +2426,10 @@ color: $m-blue-d1; } + .title { + font-weight: 400; + } + // progress nav .progress .progress-step { diff --git a/lms/templates/verify_student/_verification_header.html b/lms/templates/verify_student/_verification_header.html index 04eea86a15..519b14ae8d 100644 --- a/lms/templates/verify_student/_verification_header.html +++ b/lms/templates/verify_student/_verification_header.html @@ -2,71 +2,28 @@ <%namespace name='static' file='../static_content.html'/> - <%block name="js_extra"> diff --git a/lms/templates/verify_student/make_payment_step.underscore b/lms/templates/verify_student/make_payment_step.underscore index c97c18ae68..2c7260cd4d 100644 --- a/lms/templates/verify_student/make_payment_step.underscore +++ b/lms/templates/verify_student/make_payment_step.underscore @@ -1,4 +1,4 @@ -
+
<% if ( !upgrade ) { %>

@@ -36,6 +36,7 @@ <% } %>

+ <% if ( requirements['account-activation-required'] || requirements['photo-id-required'] || requirements['webcam-required']) { %>
    <% if ( requirements['account-activation-required'] ) { %> @@ -77,96 +78,31 @@ <% } %>
+ <% } %> - <% if ( isActive && ( !hasVisibleReqs || upgrade || !contributionAmount) ) { %> - + <% if ( courseModeSlug === 'no-id-professional') { %> +
+

<%- gettext( "ID-Verification is not required for this Professional Education course." ) %>

+

<%- gettext( "All professional education courses are fee-based, and require payment to complete the enrollment process." ) %>

+
+ <% } else if ( alreadyVerified && verificationGoodUntil ) { %> +
+

<%- gettext( "You have already verified your ID!" ) %>

+

+ <%= _.sprintf( + gettext( "Your verification status is good until %(verificationGoodUntil)s." ), + { verificationGoodUntil: verificationGoodUntil } + ) %> +

<% } %> <% if ( isActive ) { %> - % if is_active: