From 56ba7850a0c7ed8f46c0253a4e8710eaf9b8e576 Mon Sep 17 00:00:00 2001 From: alasdairswan Date: Tue, 1 Dec 2015 14:50:32 -0500 Subject: [PATCH 1/8] Integrated JS with Peter's Django wrapper Rendering form. Updated form field template to accept instructions for all input types Added setExtraData function to FormView to allow non-form attributes to be added to the model before saving --- lms/djangoapps/courseware/tests/test_views.py | 12 +- lms/djangoapps/courseware/views.py | 6 +- .../financial_assistance_form_factory.js | 17 ++ .../models/financial_assistance_model.js | 14 ++ .../financial_assessment_form.underscore | 48 ++++ .../financial_assessment_submitted.underscore | 8 + .../views/financial_assistance_form_view.js | 107 +++++++++ .../financial_assistance_form_view_spec.js | 59 +++++ .../js/student_account/views/FormView.js | 8 + .../sass/views/_financial-assistance.scss | 211 ++++++++++++++++-- .../student_account/form_field.underscore | 15 +- 11 files changed, 467 insertions(+), 38 deletions(-) create mode 100644 lms/static/js/financial-assistance/financial_assistance_form_factory.js create mode 100644 lms/static/js/financial-assistance/models/financial_assistance_model.js create mode 100644 lms/static/js/financial-assistance/templates/financial_assessment_form.underscore create mode 100644 lms/static/js/financial-assistance/templates/financial_assessment_submitted.underscore create mode 100644 lms/static/js/financial-assistance/views/financial_assistance_form_view.js create mode 100644 lms/static/js/spec/financial-assistance/financial_assistance_form_view_spec.js diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index 6f671f8f51..f2cfe41c25 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -522,15 +522,15 @@ class ViewsTestCase(ModuleStoreTestCase): effort = "I'm done, okay? You just give me my money, and you and I, we're done." data = { 'username': username, - 'course_id': course, - 'legal_name': legal_name, + 'course': course, + 'name': legal_name, 'email': self.user.email, 'country': country, 'income': income, 'reason_for_applying': reason_for_applying, 'goals': goals, 'effort': effort, - 'marketing_permission': False, + 'mktg-permission': False, } response = self._submit_financial_assistance_form(data) self.assertEqual(response.status_code, 204) @@ -560,15 +560,15 @@ class ViewsTestCase(ModuleStoreTestCase): def test_zendesk_submission_failed(self, _mock_record_feedback): response = self._submit_financial_assistance_form({ 'username': self.user.username, - 'course_id': '', - 'legal_name': '', + 'course': '', + 'name': '', 'email': '', 'country': '', 'income': '', 'reason_for_applying': '', 'goals': '', 'effort': '', - 'marketing_permission': False, + 'mktg-permission': False, }) self.assertEqual(response.status_code, 500) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 6c2ecf1ce3..72f15070c5 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -1460,15 +1460,15 @@ def financial_assistance_request(request): if request.user.username != username: return HttpResponseForbidden() - course_id = data['course_id'] - legal_name = data['legal_name'] + course_id = data['course'] + legal_name = data['name'] email = data['email'] country = data['country'] income = data['income'] reason_for_applying = data['reason_for_applying'] goals = data['goals'] effort = data['effort'] - marketing_permission = data['marketing_permission'] + marketing_permission = data['mktg-permission'] ip_address = get_ip(request) except ValueError: # Thrown if JSON parsing fails diff --git a/lms/static/js/financial-assistance/financial_assistance_form_factory.js b/lms/static/js/financial-assistance/financial_assistance_form_factory.js new file mode 100644 index 0000000000..0cc77a7617 --- /dev/null +++ b/lms/static/js/financial-assistance/financial_assistance_form_factory.js @@ -0,0 +1,17 @@ +;(function (define) { + 'use strict'; + + define([ + 'js/financial-assistance/views/financial_assistance_form_view' + ], + function (FinancialAssistanceFormView) { + return function (options) { + var formView = new FinancialAssistanceFormView({ + el: '.financial-assistance-wrapper', + context: options + }); + + return formView; + }; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/financial-assistance/models/financial_assistance_model.js b/lms/static/js/financial-assistance/models/financial_assistance_model.js new file mode 100644 index 0000000000..3039059158 --- /dev/null +++ b/lms/static/js/financial-assistance/models/financial_assistance_model.js @@ -0,0 +1,14 @@ +/** + * Model for Financial Assistance. + */ +(function (define) { + 'use strict'; + define(['backbone'], function (Backbone) { + var FinancialAssistance = Backbone.Model.extend({ + initialize: function(options) { + this.url = options.url; + } + }); + return FinancialAssistance; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/financial-assistance/templates/financial_assessment_form.underscore b/lms/static/js/financial-assistance/templates/financial_assessment_form.underscore new file mode 100644 index 0000000000..7efe95c9c0 --- /dev/null +++ b/lms/static/js/financial-assistance/templates/financial_assessment_form.underscore @@ -0,0 +1,48 @@ +

<%- gettext('Financial Assistance Application') %>

+ +
+ <% _.each(header_text, function(copy) { %> +

<%- copy %>

+ <% }); %> +
+ +
+ + + + + <%= fields %> + + +
diff --git a/lms/static/js/financial-assistance/templates/financial_assessment_submitted.underscore b/lms/static/js/financial-assistance/templates/financial_assessment_submitted.underscore new file mode 100644 index 0000000000..88897f0cfb --- /dev/null +++ b/lms/static/js/financial-assistance/templates/financial_assessment_submitted.underscore @@ -0,0 +1,8 @@ +

<%- gettext('Financial Assistance Application') %>

+

<%- interpolate_text( + gettext('Thank you for submitting your financial assistance application for {course_name}! You can expect a response in 2-4 business days.'), {course_name: course} + ) %> +

+
+ <%- gettext('Go to Dashboard') %> +
diff --git a/lms/static/js/financial-assistance/views/financial_assistance_form_view.js b/lms/static/js/financial-assistance/views/financial_assistance_form_view.js new file mode 100644 index 0000000000..1cecdb4db7 --- /dev/null +++ b/lms/static/js/financial-assistance/views/financial_assistance_form_view.js @@ -0,0 +1,107 @@ +;(function (define) { + 'use strict'; + + define(['backbone', + 'jquery', + 'underscore', + 'gettext', + 'js/financial-assistance/models/financial_assistance_model', + 'text!js/financial-assistance/templates/financial_assessment_form.underscore', + 'text!js/financial-assistance/templates/financial_assessment_submitted.underscore', + 'js/student_account/views/FormView', + 'text!templates/student_account/form_field.underscore' + ], + function(Backbone, $, _, gettext, FinancialAssistanceModel, formViewTpl, successTpl, FormView, formFieldTpl) { + return FormView.extend({ + el: '.financial-assistance-wrapper', + events: { + 'click .js-submit-form': 'submitForm' + }, + tpl: formViewTpl, + fieldTpl: formFieldTpl, + formType: 'financial-assistance', + requiredStr: '', + submitButton: '.js-submit-form', + + initialize: function(data) { + var context = data.context, + fields = context.fields; + + // Add default option to array + if ( fields[0].options.length > 1 ) { + fields[0].options.unshift({ + name: '- ' + gettext('Choose one') + ' -', + value: '', + default: true + }); + } + + // Set non-form data needed to render the View + this.context = { + dashboard_url: context.dashboard_url, + header_text: context.header_text, + platform_name: context.platform_name, + student_faq_url: context.student_faq_url + }; + + // Make the value accessible to this View + this.user_details = context.user_details; + + // Initialize the model and set user details + this.model = new FinancialAssistanceModel({ + url: context.submit_url + }); + this.model.set( context.user_details ); + this.listenTo( this.model, 'error', this.saveError ); + this.model.on('sync', this.renderSuccess, this); + + // Build the form + this.buildForm( fields ); + }, + + render: function(html) { + var data = _.extend( this.model.toJSON(), this.context, { + fields: html || '', + }); + + this.$el.html(_.template(this.tpl, data)); + + this.postRender(); + + return this; + }, + + renderSuccess: function() { + this.$el.html(_.template(successTpl, { + course: this.model.get('course'), + dashboard_url: this.context.dashboard_url + })); + + $('.js-success-message').focus(); + }, + + saveError: function(error) { + /*jslint maxlen: 500 */ + var txt = [ + 'An error has occurred. Wait a few minutes and then try to submit the application again.', + 'If you continue to have issues please contact support.' + ], + msg = gettext(txt.join(' ')); + + if (error.status === 0) { + msg = gettext('An error has occurred. Check your Internet connection and try again.'); + } + + this.errors = ['
  • ' + msg + '
  • ']; + this.setErrors(); + this.element.hide( this.$resetSuccess ); + this.toggleDisableButton(false); + }, + + setExtraData: function(data) { + return _.extend(data, this.user_details); + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/financial-assistance/financial_assistance_form_view_spec.js b/lms/static/js/spec/financial-assistance/financial_assistance_form_view_spec.js new file mode 100644 index 0000000000..9ec688e934 --- /dev/null +++ b/lms/static/js/spec/financial-assistance/financial_assistance_form_view_spec.js @@ -0,0 +1,59 @@ +define([ + 'backbone', + 'jquery', + 'js/financial-assistance/views/financial_assistance_form_view' + ], function (Backbone, $, FinancialAssistanceFormView) { + + 'use strict'; + + describe('Financial Assistance View', function () { + var view = null, + context = { + fields: [{ + defaultValue: '', + form: 'financial-assistance', + instructions: 'select a course', + label: 'Course', + name: 'course', + options: [ + {'name': 'Verified with Audit', 'value': 'course-v1:HCFA+VA101+2015'}, + {'name': 'Something Else', 'value': 'course-v1:SomethingX+SE101+215'}, + {'name': 'Test Course', 'value': 'course-v1:TestX+T101+2015'} + ], + placeholder: '', + required: true, + requiredStr: '', + type: 'select' + }], + user_details: { + country: 'UK', + email: 'xsy@edx.org', + name: 'xsy', + username: 'xsy4ever' + }, + header_text: ['Line one.', 'Line two.'], + student_faq_url: '/faqs', + dashboard_url: '/dashboard', + platform_name: 'edx', + submit_url: '/api/financial/v1/assistance' + }; + + beforeEach(function() { + setFixtures('
    '); + view = new FinancialAssistanceFormView({ + el: '.financial-assistance-wrapper', + context: context + }); + }); + + afterEach(function() { + view.undelegateEvents(); + view.remove(); + }); + + it('should exist', function() { + expect(view).toBeDefined(); + }); + }); + } +); diff --git a/lms/static/js/student_account/views/FormView.js b/lms/static/js/student_account/views/FormView.js index 75f7c56f1c..42fb614a76 100644 --- a/lms/static/js/student_account/views/FormView.js +++ b/lms/static/js/student_account/views/FormView.js @@ -213,6 +213,13 @@ this.focusFirstError(); }, + /* Allows extended views to add non-form attributes + * to the data before saving it to model + */ + setExtraData: function( data ) { + return data; + }, + submitForm: function( event ) { var data = this.getFormData(); @@ -223,6 +230,7 @@ this.toggleDisableButton(true); if ( !_.compact(this.errors).length ) { + data = this.setExtraData( data ); this.model.set( data ); this.model.save(); this.toggleErrorMsg( false ); diff --git a/lms/static/sass/views/_financial-assistance.scss b/lms/static/sass/views/_financial-assistance.scss index 4fc5bc69bd..e8415e6da9 100644 --- a/lms/static/sass/views/_financial-assistance.scss +++ b/lms/static/sass/views/_financial-assistance.scss @@ -1,33 +1,39 @@ +%fa-copy { + @extend %t-copy-base; + padding: ($baseline/2) 0; + margin: 0; + color: $m-gray-d2; +}; + .financial-assistance-wrapper { margin: auto; - padding: $baseline 0; + padding: $baseline ($baseline/2); max-width: 1180px; - .financial-assistance { + h1 { + @extend %t-title4; + @include text-align(left); + margin: 0; + padding: ($baseline/2) 0; border-bottom: 4px solid $gray-l5; + color: $m-gray-d3; + } - h1 { - @extend %t-title4; - @include text-align(left); - margin: 0; - padding: ($baseline/2) 0; - border-bottom: 4px solid $gray-l5; - color: $m-gray-d3; - } + h2 { + @extend %t-title6; + @extend %t-strong; + margin-top: ($baseline/2); + text-transform: none; + } - h2 { - @extend %t-title6; - @extend %t-strong; - margin-top: ($baseline/2); - text-transform: none; - } + p { + @extend %fa-copy; + font-size: 0.875em; + } - p { - @extend %t-copy-base; - padding: ($baseline/2) 0; - margin: 0; - color: $m-gray-d2; - } + .financial-assistance { + padding-bottom: ($baseline/2); + border-bottom: 4px solid $gray-l5; .apply-form-list { padding: 0; @@ -73,4 +79,165 @@ border-radius: 2px; } } + + // Application form View + .intro { + border-bottom: 4px solid $gray-l5; + + p { + margin: 10px 0; + } + } + + .success-message { + p { + margin: 10px 0; + } + } + + .btn-dashboard { + @include float(right); + color: $white; + + &:hover, + &:active, + &:focus { + color: $white; + } + } + + .user-info { + @include clearfix(); + border-bottom: 2px solid $gray-l5; + padding: 20px 0; + margin-bottom: 20px; + + .info-column { + @include float(left); + width: 100%; + margin: 10px 0; + } + + .title { + @extend %fa-copy; + padding: 0; + } + + .data { + @extend %fa-copy; + padding: 0; + color: $black; + font-size: 1.125em; + } + } + + .financial-assistance-form { + @extend .login-register; + + .action-primary { + @include float(left); + width: auto; + margin-top: 0; + } + + .nav-link { + margin: 15px 0; + display: block; + } + + form { + border: none; + } + + .form-field { + select, + input { + width: 320px; + } + + input { + border: { + top: none; + right: none; + bottom: 3px solid $gray-l1; + left: none; + }; + box-shadow: none; + } + + textarea { + height: 125px; + } + + .checkbox { + height: auto; + position: absolute; + top: 5px; + + & + label { + @include margin-left(30px); + display: inline-block; + } + } + } + } + + .cta-wrapper { + border-top: 4px solid $gray-l5; + padding: 20px 0; + } + + @include media($bp-medium) { + .user-info { + .info-column { + width: 50%; + } + } + + .financial-assistance-form { + .action-primary { + @include float(right); + } + + .nav-link { + display: inline-block; + } + } + } + + @include media($bp-large) { + .user-info { + .info-column { + width: 25%; + } + } + + .financial-assistance-form { + .action-primary { + @include float(right); + } + + .nav-link { + display: inline-block; + } + } + } + + @include media($bp-huge) { + .user-info { + .info-column { + width: 25%; + } + } + + .financial-assistance-form { + .action-primary { + @include float(right); + } + + .nav-link { + display: inline-block; + } + } + } } diff --git a/lms/templates/student_account/form_field.underscore b/lms/templates/student_account/form_field.underscore index 943b070940..898a59b5e6 100644 --- a/lms/templates/student_account/form_field.underscore +++ b/lms/templates/student_account/form_field.underscore @@ -21,6 +21,7 @@ <% }); %> + <% if ( instructions ) { %> <%= instructions %><% } %> <% } else if ( type === 'textarea' ) { %> + <% if ( instructions ) { %> <%= instructions %><% } %> <% } else { %> placeholder="<%= placeholder %>"<% } %> value="<%- defaultValue %>" /> + <% if ( type === 'checkbox' ) { %> + + <% } %> <% if ( instructions ) { %> <%= instructions %><% } %> <% } %> - <% if ( type === 'checkbox' ) { %> - - <% } %> - <% if( form === 'login' && name === 'password' ) { %> <%- gettext("Forgot password?") %> <% } %> From 1592a32de3270cf6eb41d48b41ec0a02ba334cd5 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Sat, 5 Dec 2015 18:00:16 -0500 Subject: [PATCH 2/8] White Labels need to still use the 'honor' course mode even through the default has changed --- common/djangoapps/course_modes/admin.py | 4 +++- common/djangoapps/course_modes/models.py | 7 +++++++ lms/djangoapps/instructor/enrollment.py | 11 ++++++++++- .../instructor_task/tests/test_tasks_helper.py | 15 ++++++++++++--- lms/djangoapps/shoppingcart/models.py | 17 ++++++++++------- .../shoppingcart/tests/test_models.py | 4 ++-- lms/djangoapps/shoppingcart/tests/test_views.py | 8 +------- 7 files changed, 45 insertions(+), 21 deletions(-) diff --git a/common/djangoapps/course_modes/admin.py b/common/djangoapps/course_modes/admin.py index 6781227f5c..8356f9134d 100644 --- a/common/djangoapps/course_modes/admin.py +++ b/common/djangoapps/course_modes/admin.py @@ -39,7 +39,9 @@ class CourseModeForm(forms.ModelForm): [(CourseMode.DEFAULT_MODE_SLUG, CourseMode.DEFAULT_MODE_SLUG)] + [(mode_slug, mode_slug) for mode_slug in CourseMode.VERIFIED_MODES] + [(CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.NO_ID_PROFESSIONAL_MODE)] + - [(mode_slug, mode_slug) for mode_slug in CourseMode.CREDIT_MODES] + [(mode_slug, mode_slug) for mode_slug in CourseMode.CREDIT_MODES] + + # need to keep legacy modes around for awhile + [(CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG, CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG)] ) mode_slug = forms.ChoiceField(choices=COURSE_MODE_SLUG_CHOICES, label=_("Mode")) diff --git a/common/djangoapps/course_modes/models.py b/common/djangoapps/course_modes/models.py index 4402a3e00a..022de21c21 100644 --- a/common/djangoapps/course_modes/models.py +++ b/common/djangoapps/course_modes/models.py @@ -114,6 +114,13 @@ class CourseMode(models.Model): # Modes that are allowed to upsell UPSELL_TO_VERIFIED_MODES = [HONOR, AUDIT] + # Courses purchased through the shoppingcart + # should be "honor". Since we've changed the DEFAULT_MODE_SLUG from + # "honor" to "audit", we still need to have the shoppingcart + # use "honor" + DEFAULT_SHOPPINGCART_MODE_SLUG = HONOR + DEFAULT_SHOPPINGCART_MODE = Mode(HONOR, _('Honor'), 0, '', 'usd', None, None, None) + class Meta(object): unique_together = ('course_id', 'mode_slug', 'currency') diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py index d963721508..e134964fa2 100644 --- a/lms/djangoapps/instructor/enrollment.py +++ b/lms/djangoapps/instructor/enrollment.py @@ -111,7 +111,16 @@ def enroll_email(course_id, student_email, auto_enroll=False, email_students=Fal if previous_state.user: # if the student is currently unenrolled, don't enroll them in their # previous mode - course_mode = CourseMode.DEFAULT_MODE_SLUG + + # for now, White Labels use 'shoppingcart' which is based on the + # "honor" course_mode. Given the change to use "audit" as the default + # course_mode in Open edX, we need to be backwards compatible with + # how White Labels approach enrollment modes. + if CourseMode.is_white_label(course_id): + course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG + else: + course_mode = CourseMode.DEFAULT_MODE_SLUG + if previous_state.enrollment: course_mode = previous_state.mode diff --git a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py index 9a51b00835..6a8b087f39 100644 --- a/lms/djangoapps/instructor_task/tests/test_tasks_helper.py +++ b/lms/djangoapps/instructor_task/tests/test_tasks_helper.py @@ -362,6 +362,11 @@ class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCour def setUp(self): super(TestInstructorDetailedEnrollmentReport, self).setUp() self.course = CourseFactory.create() + CourseModeFactory.create( + course_id=self.course.id, + min_price=50, + mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG + ) # create testing invoice 1 self.instructor = InstructorFactory(course_key=self.course.id) @@ -476,7 +481,7 @@ class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCour created_by=self.instructor, invoice=self.sale_invoice_1, invoice_item=self.invoice_item, - mode_slug=CourseMode.DEFAULT_MODE_SLUG + mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG ) course_registration_code.save() @@ -517,7 +522,7 @@ class TestInstructorDetailedEnrollmentReport(TestReportMixin, InstructorTaskCour created_by=self.instructor, invoice=self.sale_invoice_1, invoice_item=self.invoice_item, - mode_slug=CourseMode.DEFAULT_MODE_SLUG + mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG ) course_registration_code.save() @@ -845,7 +850,11 @@ class TestExecutiveSummaryReport(TestReportMixin, InstructorTaskCourseTestCase): def setUp(self): super(TestExecutiveSummaryReport, self).setUp() self.course = CourseFactory.create() - CourseModeFactory.create(course_id=self.course.id, min_price=50) + CourseModeFactory.create( + course_id=self.course.id, + min_price=50, + mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG + ) self.instructor = InstructorFactory(course_key=self.course.id) self.student1 = UserFactory() diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 9a935a2d89..ae74ee9d46 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -1,3 +1,4 @@ +# pylint: disable=arguments-differ """ Models for the shopping cart and assorted purchase types """ from collections import namedtuple @@ -1473,7 +1474,7 @@ class PaidCourseRegistration(OrderItem): app_label = "shoppingcart" course_id = CourseKeyField(max_length=128, db_index=True) - mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG) + mode = models.SlugField(default=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG) course_enrollment = models.ForeignKey(CourseEnrollment, null=True) @classmethod @@ -1526,7 +1527,8 @@ class PaidCourseRegistration(OrderItem): @classmethod @transaction.atomic - def add_to_order(cls, order, course_id, mode_slug=CourseMode.DEFAULT_MODE_SLUG, cost=None, currency=None): + def add_to_order(cls, order, course_id, mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG, + cost=None, currency=None): # pylint: disable=arguments-differ """ A standardized way to create these objects, with sensible defaults filled in. Will update the cost if called on an order that already carries the course. @@ -1561,7 +1563,7 @@ class PaidCourseRegistration(OrderItem): course_mode = CourseMode.mode_for_course(course_id, mode_slug) if not course_mode: # user could have specified a mode that's not set, in that case return the DEFAULT_MODE - course_mode = CourseMode.DEFAULT_MODE + course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE if not cost: cost = course_mode.min_price if not currency: @@ -1660,7 +1662,7 @@ class CourseRegCodeItem(OrderItem): app_label = "shoppingcart" course_id = CourseKeyField(max_length=128, db_index=True) - mode = models.SlugField(default=CourseMode.DEFAULT_MODE_SLUG) + mode = models.SlugField(default=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG) @classmethod def get_bulk_purchased_seat_count(cls, course_key, status='purchased'): @@ -1706,7 +1708,8 @@ class CourseRegCodeItem(OrderItem): @classmethod @transaction.atomic - def add_to_order(cls, order, course_id, qty, mode_slug=CourseMode.DEFAULT_MODE_SLUG, cost=None, currency=None): # pylint: disable=arguments-differ + def add_to_order(cls, order, course_id, qty, mode_slug=CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG, + cost=None, currency=None): # pylint: disable=arguments-differ """ A standardized way to create these objects, with sensible defaults filled in. Will update the cost if called on an order that already carries the course. @@ -1736,8 +1739,8 @@ class CourseRegCodeItem(OrderItem): ### handle default arguments for mode_slug, cost, currency course_mode = CourseMode.mode_for_course(course_id, mode_slug) if not course_mode: - # user could have specified a mode that's not set, in that case return the DEFAULT_MODE - course_mode = CourseMode.DEFAULT_MODE + # user could have specified a mode that's not set, in that case return the DEFAULT_SHOPPINGCART_MODE + course_mode = CourseMode.DEFAULT_SHOPPINGCART_MODE if not cost: cost = course_mode.min_price if not currency: diff --git a/lms/djangoapps/shoppingcart/tests/test_models.py b/lms/djangoapps/shoppingcart/tests/test_models.py index 98db8769b7..29c3352739 100644 --- a/lms/djangoapps/shoppingcart/tests/test_models.py +++ b/lms/djangoapps/shoppingcart/tests/test_models.py @@ -732,7 +732,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): self.assertEqual(reg1.unit_cost, 0) self.assertEqual(reg1.line_cost, 0) - self.assertEqual(reg1.mode, CourseMode.DEFAULT_MODE_SLUG) + self.assertEqual(reg1.mode, CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG) self.assertEqual(reg1.user, self.user) self.assertEqual(reg1.status, "cart") self.assertEqual(self.cart.total_cost, 0) @@ -742,7 +742,7 @@ class PaidCourseRegistrationTest(ModuleStoreTestCase): self.assertEqual(course_reg_code_item.unit_cost, 0) self.assertEqual(course_reg_code_item.line_cost, 0) - self.assertEqual(course_reg_code_item.mode, CourseMode.DEFAULT_MODE_SLUG) + self.assertEqual(course_reg_code_item.mode, CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG) self.assertEqual(course_reg_code_item.user, self.user) self.assertEqual(course_reg_code_item.status, "cart") self.assertEqual(self.cart.total_cost, 0) diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py index 381277ed52..56ea2adff7 100644 --- a/lms/djangoapps/shoppingcart/tests/test_views.py +++ b/lms/djangoapps/shoppingcart/tests/test_views.py @@ -247,13 +247,7 @@ class ShoppingCartViewsTests(SharedModuleStoreTestCase, XssTestMixin): test to check that that the same coupon code applied on multiple items in the cart. """ - for course_key, cost in ((self.course_key, 40), (self.testing_course.id, 20)): - CourseMode( - course_id=course_key, - mode_slug=CourseMode.DEFAULT_MODE_SLUG, - mode_display_name=CourseMode.DEFAULT_MODE_SLUG, - min_price=cost - ).save() + self.login_user() # add first course to user cart resp = self.client.post( From 06442036ce2dfa5b6931b07454bd963818a7c6b9 Mon Sep 17 00:00:00 2001 From: Mushtaq Ali Date: Thu, 3 Dec 2015 18:49:44 +0500 Subject: [PATCH 3/8] Load requirejs before module-js --- lms/templates/main.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lms/templates/main.html b/lms/templates/main.html index 1e450583ec..cb04bb7f24 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -69,7 +69,6 @@ from branding import api as branding_api % else: <%static:js group='main_vendor'/> <%static:js group='application'/> - <%static:js group='module-js'/> % endif + % if not disable_courseware_js: + <%static:js group='module-js'/> + % endif + <%block name="headextra"/> <% From f4023e821f4e2c32ffa5813ab4b7d4b12195005d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 7 Dec 2015 10:26:47 -0500 Subject: [PATCH 4/8] Use `disable_footer` variable to disable footer on login page Rather than Mako block hack --- lms/djangoapps/student_account/views.py | 1 + lms/templates/student_account/login_and_register.html | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lms/djangoapps/student_account/views.py b/lms/djangoapps/student_account/views.py index 8265e9fa8b..bcc156d6d1 100644 --- a/lms/djangoapps/student_account/views.py +++ b/lms/djangoapps/student_account/views.py @@ -116,6 +116,7 @@ def login_and_registration_form(request, initial_mode="login"): 'responsive': True, 'allow_iframing': True, 'disable_courseware_js': True, + 'disable_footer': True, } return render_to_response('student_account/login_and_register.html', context) diff --git a/lms/templates/student_account/login_and_register.html b/lms/templates/student_account/login_and_register.html index d633d64965..962936bb93 100644 --- a/lms/templates/student_account/login_and_register.html +++ b/lms/templates/student_account/login_and_register.html @@ -27,10 +27,3 @@
    - -% if settings.FEATURES.get('ENABLE_COMBINED_LOGIN_REGISTRATION'): - ## This overwrites the "footer" block declared in main.html - ## with an empty block, effectively hiding the footer - ## from logistration pages. - <%block name="footer"/> -% endif From 1a2ffbda68364516af54955e7f12f60ed26d5632 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 7 Dec 2015 13:52:52 -0500 Subject: [PATCH 5/8] Disable footer for courseware-chromeless.html --- lms/djangoapps/courseware/views.py | 1 + lms/templates/courseware/courseware-chromeless.html | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 72f15070c5..9625fffbda 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -1403,6 +1403,7 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): 'disable_accordion': True, 'allow_iframing': True, 'disable_header': True, + 'disable_footer': True, 'disable_window_wrap': True, 'disable_preview_menu': True, 'staff_access': bool(has_access(request.user, 'staff', course)), diff --git a/lms/templates/courseware/courseware-chromeless.html b/lms/templates/courseware/courseware-chromeless.html index 5d00ad9232..fec184af07 100644 --- a/lms/templates/courseware/courseware-chromeless.html +++ b/lms/templates/courseware/courseware-chromeless.html @@ -107,6 +107,3 @@ ${fragment.foot_html()} <%include file="../modal/accessible_confirm.html" /> - -## No footer in chromeless -<%block name="footer"> From 692e5d777ac20403b2f0c9fa9ef964b8b530de8f Mon Sep 17 00:00:00 2001 From: Bill DeRusha Date: Mon, 7 Dec 2015 10:24:29 -0500 Subject: [PATCH 6/8] Update copy for HCFA Update Zendesk ticket to correctly add group. Add logic to hide new audit certs --- common/djangoapps/student/views.py | 7 +- common/djangoapps/util/views.py | 64 +++++++++++++-- common/templates/course_modes/choose.html | 11 +-- lms/djangoapps/certificates/models.py | 11 +++ lms/djangoapps/courseware/tests/test_views.py | 30 +++---- lms/djangoapps/courseware/views.py | 79 +++++++++---------- lms/djangoapps/mobile_api/users/tests.py | 5 ++ .../_dashboard_certificate_information.html | 3 +- .../financial-assistance.html | 4 +- 9 files changed, 140 insertions(+), 74 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 506dc069a6..c3896022f2 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -295,6 +295,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa CertificateStatuses.downloadable: 'ready', CertificateStatuses.notpassing: 'notpassing', CertificateStatuses.restricted: 'restricted', + CertificateStatuses.auditing: 'auditing', } default_status = 'processing' @@ -309,7 +310,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa if cert_status is None: return default_info - is_hidden_status = cert_status['status'] in ('unavailable', 'processing', 'generating', 'notpassing') + is_hidden_status = cert_status['status'] in ('unavailable', 'processing', 'generating', 'notpassing', 'auditing') if course_overview.certificates_display_behavior == 'early_no_info' and is_hidden_status: return {} @@ -325,7 +326,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa 'can_unenroll': status not in DISABLE_UNENROLL_CERT_STATES, } - if (status in ('generating', 'ready', 'notpassing', 'restricted') and + if (status in ('generating', 'ready', 'notpassing', 'restricted', 'auditing') and course_overview.end_of_course_survey_url is not None): status_dict.update({ 'show_survey_button': True, @@ -373,7 +374,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa cert_status['download_url'] ) - if status in ('generating', 'ready', 'notpassing', 'restricted'): + if status in ('generating', 'ready', 'notpassing', 'restricted', 'auditing'): if 'grade' not in cert_status: # Note: as of 11/20/2012, we know there are students in this state-- cs169.1x, # who need to be regraded (we weren't tracking 'notpassing' at first). diff --git a/common/djangoapps/util/views.py b/common/djangoapps/util/views.py index 99083db7fc..aac618862d 100644 --- a/common/djangoapps/util/views.py +++ b/common/djangoapps/util/views.py @@ -4,6 +4,7 @@ import sys from functools import wraps from django.conf import settings +from django.core.cache import caches from django.core.validators import ValidationError, validate_email from django.views.decorators.csrf import requires_csrf_token from django.views.defaults import server_error @@ -115,6 +116,10 @@ def calculate(request): class _ZendeskApi(object): + + CACHE_PREFIX = 'ZENDESK_API_CACHE' + CACHE_TIMEOUT = 60 * 60 + def __init__(self): """ Instantiate the Zendesk API. @@ -150,8 +155,39 @@ class _ZendeskApi(object): """ self._zendesk_instance.update_ticket(ticket_id=ticket_id, data=update) + def get_group(self, name): + """ + Find the Zendesk group named `name`. Groups are cached for + CACHE_TIMEOUT seconds. -def _record_feedback_in_zendesk(realname, email, subject, details, tags, additional_info): + If a matching group exists, it is returned as a dictionary + with the format specifed by the zendesk package. + + Otherwise, returns None. + """ + cache = caches['default'] + cache_key = '{prefix}_group_{name}'.format(prefix=self.CACHE_PREFIX, name=name) + cached = cache.get(cache_key) + if cached: + return cached + groups = self._zendesk_instance.list_groups()['groups'] + for group in groups: + if group['name'] == name: + cache.set(cache_key, group, self.CACHE_TIMEOUT) + return group + return None + + +def _record_feedback_in_zendesk( + realname, + email, + subject, + details, + tags, + additional_info, + group_name=None, + require_update=False +): """ Create a new user-requested Zendesk ticket. @@ -159,6 +195,12 @@ def _record_feedback_in_zendesk(realname, email, subject, details, tags, additio additional information from the browser and server, such as HTTP headers and user state. Returns a boolean value indicating whether ticket creation was successful, regardless of whether the private comment update succeeded. + + If `group_name` is provided, attaches the ticket to the matching Zendesk group. + + If `require_update` is provided, returns False when the update does not + succeed. This allows using the private comment to add necessary information + which the user will not see in followup emails from support. """ zendesk_api = _ZendeskApi() @@ -184,8 +226,18 @@ def _record_feedback_in_zendesk(realname, email, subject, details, tags, additio "tags": zendesk_tags } } + group = None + if group_name is not None: + group = zendesk_api.get_group(group_name) + if group is not None: + new_ticket['ticket']['group_id'] = group['id'] try: ticket_id = zendesk_api.create_ticket(new_ticket) + if group is None: + # Support uses Zendesk groups to track tickets. In case we + # haven't been able to correctly group this ticket, log its ID + # so it can be found later. + log.warning('Unable to find group named %s for Zendesk ticket with ID %s.', group_name, ticket_id) except zendesk.ZendeskError: log.exception("Error creating Zendesk ticket") return False @@ -196,10 +248,12 @@ def _record_feedback_in_zendesk(realname, email, subject, details, tags, additio try: zendesk_api.update_ticket(ticket_id, ticket_update) except zendesk.ZendeskError: - log.exception("Error updating Zendesk ticket") - # The update is not strictly necessary, so do not indicate failure to the user - pass - + log.exception("Error updating Zendesk ticket with ID %s.", ticket_id) + # The update is not strictly necessary, so do not indicate + # failure to the user unless it has been requested with + # `require_update`. + if require_update: + return False return True diff --git a/common/templates/course_modes/choose.html b/common/templates/course_modes/choose.html index d6564a9a6d..ddddcd0390 100644 --- a/common/templates/course_modes/choose.html +++ b/common/templates/course_modes/choose.html @@ -143,15 +143,15 @@ from django.core.urlresolvers import reverse
    -

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

    +

    ${_("Audit This Course")}

    -

    ${_("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.")}

    +

    ${_("Audit this course for free and have complete access to all the course material, activities, tests, and forums.")}

    • - +
    @@ -163,9 +163,10 @@ from django.core.urlresolvers import reverse
    -

    ${_("Audit This Course")}

    +

    ${_("Audit This Course (No Certificate)")}

    -

    ${_("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.")}

    + ## Translators: b_start notes the beginning of a section of text bolded for emphasis, and b_end marks the end of the bolded text. +

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

    diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 20d70c42db..839d1912a7 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -86,6 +86,7 @@ class CertificateStatuses(object): regenerating = 'regenerating' restricted = 'restricted' unavailable = 'unavailable' + auditing = 'auditing' class CertificateSocialNetworks(object): @@ -305,10 +306,20 @@ def certificate_status_for_student(student, course_id): } if generated_certificate.grade: cert_status['grade'] = generated_certificate.grade + + if generated_certificate.mode == 'audit': + course_mode_slugs = [mode.slug for mode in CourseMode.modes_for_course(course_id)] + # Short term fix to make sure old audit users with certs still see their certs + # only do this if there if no honor mode + if 'honor' not in course_mode_slugs: + cert_status['status'] = CertificateStatuses.auditing + return cert_status + if generated_certificate.status == CertificateStatuses.downloadable: cert_status['download_url'] = generated_certificate.download_url return cert_status + except GeneratedCertificate.DoesNotExist: pass return {'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor} diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index f2cfe41c25..ef87ea0844 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -536,31 +536,32 @@ class ViewsTestCase(ModuleStoreTestCase): self.assertEqual(response.status_code, 204) __, ___, ticket_subject, ticket_body, tags, additional_info = mock_record_feedback.call_args[0] - for info in (country, income, reason_for_applying, goals, effort): - self.assertIn(info, ticket_body) - self.assertIn('This user HAS NOT allowed this content to be used for edX marketing purposes.', ticket_body) + mocked_kwargs = mock_record_feedback.call_args[1] + group_name = mocked_kwargs['group_name'] + require_update = mocked_kwargs['require_update'] + private_comment = '\n'.join(additional_info.values()) + for info in (country, income, reason_for_applying, goals, effort, username, legal_name, course): + self.assertIn(info, private_comment) + + self.assertEqual(additional_info['Allowed for marketing purposes'], 'No') self.assertEqual( ticket_subject, - 'Financial assistance request for user {username} in course {course}'.format( + 'Financial assistance request for learner {username} in course {course}'.format( username=username, - course=course + course=self.course.display_name ) ) - self.assertDictContainsSubset( - { - 'issue_type': 'Financial Assistance', - 'course_id': course - }, - tags - ) + self.assertDictContainsSubset({'course_id': course}, tags) self.assertIn('Client IP', additional_info) + self.assertEqual(group_name, 'Financial Assistance') + self.assertTrue(require_update) @patch.object(views, '_record_feedback_in_zendesk', return_value=False) def test_zendesk_submission_failed(self, _mock_record_feedback): response = self._submit_financial_assistance_form({ 'username': self.user.username, - 'course': '', + 'course': unicode(self.course.id), 'name': '', 'email': '', 'country': '', @@ -574,7 +575,8 @@ class ViewsTestCase(ModuleStoreTestCase): @ddt.data( ({}, 400), - ({'username': 'wwhite'}, 403) + ({'username': 'wwhite'}, 403), + ({'username': 'dummy', 'course': 'bad course ID'}, 400) ) @ddt.unpack def test_submit_financial_assistance_errors(self, data, status): diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 72f15070c5..8742e3f4e8 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -7,6 +7,7 @@ import json import textwrap import urllib +from collections import OrderedDict from datetime import datetime from django.utils.translation import ugettext as _ @@ -1414,20 +1415,22 @@ def render_xblock(request, usage_key_string, check_if_enrolled=True): # Translators: "percent_sign" is the symbol "%". "platform_name" is a # string identifying the name of this installation, such as "edX". FINANCIAL_ASSISTANCE_HEADER = _( - '{platform_name} now offers financial assistance for learners who want to earn verified certificates but' + '{platform_name} now offers financial assistance for learners who want to earn Verified Certificates but' ' who may not be able to pay the Verified Certificate fee. Eligible learners receive 90{percent_sign} off' ' the Verified Certificate fee for a course.\nTo apply for financial assistance, enroll in the' ' audit track for a course that offers Verified Certificates, and then complete this application.' - ' Note that you must complete a separate application for each course you take.' + ' Note that you must complete a separate application for each course you take.\n We will use this' + 'information to evaluate your application for financial assistance and to further develop our' + 'financial assistance program.' ).format( percent_sign="%", platform_name=settings.PLATFORM_NAME ).split('\n') -FA_INCOME_LABEL = _('Annual Income') +FA_INCOME_LABEL = _('Annual Household Income') FA_REASON_FOR_APPLYING_LABEL = _( - 'Tell us about your current financial situation, including any unusual circumstances.' + 'Tell us about your current financial situation.' ) FA_GOALS_LABEL = _( 'Tell us about your learning or professional goals. How will a Verified Certificate in' @@ -1435,7 +1438,7 @@ FA_GOALS_LABEL = _( ) FA_EFFORT_LABEL = _( 'Tell us about your plans for this course. What steps will you take to help you complete' - ' the course work a receive a certificate?' + ' the course work and receive a certificate?' ) FA_SHORT_ANSWER_INSTRUCTIONS = _('Use between 250 and 500 words or so in your response.') @@ -1461,6 +1464,7 @@ def financial_assistance_request(request): return HttpResponseForbidden() course_id = data['course'] + course = modulestore().get_course(CourseKey.from_string(course_id)) legal_name = data['name'] email = data['email'] country = data['country'] @@ -1473,52 +1477,40 @@ def financial_assistance_request(request): except ValueError: # Thrown if JSON parsing fails return HttpResponseBadRequest('Could not parse request JSON.') + except InvalidKeyError: + # Thrown if course key parsing fails + return HttpResponseBadRequest('Could not parse request course key.') except KeyError as err: # Thrown if fields are missing return HttpResponseBadRequest('The field {} is required.'.format(err.message)) - ticket_body = textwrap.dedent( - ''' - Annual Income: {income} - Country: {country} - - {reason_label} - {separator} - {reason_for_applying} - - {goals_label} - {separator} - {goals} - - {effort_label} - {separator} - {effort} - - This user {allowed_for_marketing} allowed this content to be used for edX marketing purposes. - '''.format( - income=income, - country=country, - reason_label=FA_REASON_FOR_APPLYING_LABEL, - reason_for_applying=reason_for_applying, - goals_label=FA_GOALS_LABEL, - goals=goals, - effort_label=FA_EFFORT_LABEL, - effort=effort, - allowed_for_marketing='HAS' if marketing_permission else 'HAS NOT', - separator='=' * 16 - ) - ) - zendesk_submitted = _record_feedback_in_zendesk( legal_name, email, - 'Financial assistance request for user {username} in course {course_id}'.format( + 'Financial assistance request for learner {username} in course {course_name}'.format( username=username, - course_id=course_id + course_name=course.display_name ), - ticket_body, - {'issue_type': 'Financial Assistance', 'course_id': course_id}, - {'Client IP': ip_address} + 'Financial Assistance Request', + {'course_id': course_id}, + # Send the application as additional info on the ticket so + # that it is not shown when support replies. This uses + # OrderedDict so that information is presented in the right + # order. + OrderedDict(( + ('Username', username), + ('Full Name', legal_name), + ('Course ID', course_id), + ('Annual Household Income', income), + ('Country', country), + ('Allowed for marketing purposes', 'Yes' if marketing_permission else 'No'), + (FA_REASON_FOR_APPLYING_LABEL, '\n' + reason_for_applying + '\n\n'), + (FA_GOALS_LABEL, '\n' + goals + '\n\n'), + (FA_EFFORT_LABEL, '\n' + effort + '\n\n'), + ('Client IP', ip_address), + )), + group_name='Financial Assistance', + require_update=True ) if not zendesk_submitted: @@ -1629,7 +1621,8 @@ def financial_assistance_form(request): 'type': 'checkbox', 'required': False, 'instructions': _( - 'Annual income and personal information such as email address will not be shared.' + 'Annual income and personal information such as email address will not be shared. ' + 'Financial information will not be used for marketing purposes.' ), 'restrictions': {} } diff --git a/lms/djangoapps/mobile_api/users/tests.py b/lms/djangoapps/mobile_api/users/tests.py index 37e8ec6ace..404645e639 100644 --- a/lms/djangoapps/mobile_api/users/tests.py +++ b/lms/djangoapps/mobile_api/users/tests.py @@ -205,6 +205,11 @@ class TestUserEnrollmentApi(UrlResetMixin, MobileAPITestCase, MobileAuthUserTest @patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True}) def test_web_certificate(self): + CourseMode.objects.create( + course_id=self.course.id, + mode_display_name="Honor", + mode_slug=CourseMode.HONOR, + ) self.login_and_enroll() self.course.cert_html_view_enabled = True diff --git a/lms/templates/dashboard/_dashboard_certificate_information.html b/lms/templates/dashboard/_dashboard_certificate_information.html index bf6f716a5b..13109ea82e 100644 --- a/lms/templates/dashboard/_dashboard_certificate_information.html +++ b/lms/templates/dashboard/_dashboard_certificate_information.html @@ -27,10 +27,9 @@ else: status_css_class = 'course-status-processing' %>
    - % if cert_status['status'] == 'processing':

    ${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}

    -% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'): +% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted', 'auditing'):

    ${_("Your final grade:")} ${"{0:.0f}%".format(float(cert_status['grade'])*100)}. % if cert_status['status'] == 'notpassing': diff --git a/lms/templates/financial-assistance/financial-assistance.html b/lms/templates/financial-assistance/financial-assistance.html index e611f8453b..04ef3efabb 100644 --- a/lms/templates/financial-assistance/financial-assistance.html +++ b/lms/templates/financial-assistance/financial-assistance.html @@ -19,9 +19,9 @@ from edxmako.shortcuts import marketing_link

    ${_("A Note to Learners")}

    ${_("Dear edX Learner,")}

    ${_("EdX Financial Assistance is a program we created to give learners in all financial circumstances a chance to earn a Verified Certificate upon successful completion of an edX course.")}

    -

    ${_("If you are interested in working toward a Verified Certificate, but cannot afford to pay the fee, please apply now. Please note space is limited.")}

    +

    ${_("If you are interested in working toward a Verified Certificate, but cannot afford to pay the fee, please apply now. Please note financial assistance is limited.")}

    ${_("In order to be eligible for edX Financial Assistance, you must demonstrate that paying the Verified Certificate fee would cause you economic hardship. To apply, you will be asked to answer a few questions about why you are applying and how the Verified Certificate will benefit you.")}

    -

    ${_("Once your application is approved, we'll email to let you know and give you instructions for how to verify your identity on edX.org; then you can start working toward completing your edX course.")}

    +

    ${_("If your application is approved, we'll give you instructions for verifying your identity on edx.org so you can start working toward completing your edX course.")}

    ${_("EdX is committed to making it possible for you to take high quality courses from leading institutions regardless of your financial situation, earn a Verified Certificate, and share your success with others.")}

    ${_("Sincerely, Anant")}

    From 4117064a2f77f5880ec23aeb1d4e53e12b8ef952 Mon Sep 17 00:00:00 2001 From: AlasdairSwan Date: Mon, 7 Dec 2015 15:05:38 -0500 Subject: [PATCH 7/8] Added fa factory to rjs bundler --- lms/static/lms/js/build.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index ec66f97ad0..93dc2b0b03 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -21,6 +21,7 @@ 'js/discovery/discovery_factory', 'js/edxnotes/views/notes_visibility_factory', 'js/edxnotes/views/page_factory', + 'js/financial-assistance/financial_assistance_form_factory', 'js/groups/views/cohorts_dashboard_factory', 'js/search/course/course_search_factory', 'js/search/dashboard/dashboard_search_factory', From b05488f826b0a2ce9744f1077e03f14f7b154c3b Mon Sep 17 00:00:00 2001 From: Bill DeRusha Date: Mon, 7 Dec 2015 17:34:58 -0500 Subject: [PATCH 8/8] Update spacing in FA form --- lms/djangoapps/courseware/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index e196cec545..b86b0f3b91 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -1421,8 +1421,8 @@ FINANCIAL_ASSISTANCE_HEADER = _( ' the Verified Certificate fee for a course.\nTo apply for financial assistance, enroll in the' ' audit track for a course that offers Verified Certificates, and then complete this application.' ' Note that you must complete a separate application for each course you take.\n We will use this' - 'information to evaluate your application for financial assistance and to further develop our' - 'financial assistance program.' + ' information to evaluate your application for financial assistance and to further develop our' + ' financial assistance program.' ).format( percent_sign="%", platform_name=settings.PLATFORM_NAME