From 4911bf805f595a290ee165e638057783ed3cd98f Mon Sep 17 00:00:00 2001 From: Simon Chen Date: Tue, 28 Jun 2016 12:57:25 -0400 Subject: [PATCH] ECOM-4219 - Add the course states to course cards and make sure the display follows course states (#12844) Please enter the commit message for your changes. Lines starting --- .../models/course_card_model.js | 58 +++++++++++--- .../models/course_enroll_model.js | 2 +- .../views/course_enroll_view.js | 16 ++-- .../course_card_view_spec.js | 31 +++++++- .../course_enroll_view_spec.js | 17 +++-- lms/static/sass/elements/_course-card.scss | 28 +++++-- .../learner_dashboard/course_card.underscore | 36 ++++++--- .../course_enroll.underscore | 76 +++++++++++++------ .../djangoapps/programs/tests/test_utils.py | 20 ++++- openedx/core/djangoapps/programs/utils.py | 4 + 10 files changed, 220 insertions(+), 68 deletions(-) diff --git a/lms/static/js/learner_dashboard/models/course_card_model.js b/lms/static/js/learner_dashboard/models/course_card_model.js index 7407db7e20..05eacb8aed 100644 --- a/lms/static/js/learner_dashboard/models/course_card_model.js +++ b/lms/static/js/learner_dashboard/models/course_card_model.js @@ -15,19 +15,46 @@ } }, + getUnselectedRunMode: function(runModes) { + if(runModes && runModes.length > 0){ + return { + course_image_url: runModes[0].course_image_url, + marketing_url: runModes[0].marketing_url, + is_enrollment_open: runModes[0].is_enrollment_open, + enrollment_open_date: runModes[0].enrollment_open_date + }; + } + return {}; + }, + getRunMode: function(runModes){ - //we should populate our model by looking at the run_modes - if (runModes.length > 0){ - if(runModes.length === 1){ - return runModes[0]; + var enrolled_mode = _.findWhere(runModes, {is_enrolled: true}), + openEnrollmentRunModes = this.getEnrollableRunModes(), + desiredRunMode; + //we populate our model by looking at the run_modes + if (enrolled_mode){ + // If we have a run_mode we are already enrolled in, + // return that one always + desiredRunMode = enrolled_mode; + } else if (openEnrollmentRunModes.length > 0){ + if(openEnrollmentRunModes.length === 1){ + desiredRunMode = openEnrollmentRunModes[0]; }else{ - //We need to implement logic here to select the - //most relevant run mode for the student to enroll - return runModes[0]; + desiredRunMode = this.getUnselectedRunMode(openEnrollmentRunModes); } }else{ - return null; - } + desiredRunMode = this.getUnselectedRunMode(runModes); + } + return desiredRunMode; + }, + + getEnrollableRunModes: function(){ + return _.where(this.context.run_modes, + { + is_enrollment_open: true, + is_enrolled: false, + is_course_ended: false + }); }, setActiveRunMode: function(runMode){ @@ -38,18 +65,29 @@ course_key: runMode.course_key, course_url: runMode.course_url || '', display_name: this.context.display_name, + start_date: runMode.start_date, end_date: runMode.end_date, is_enrolled: runMode.is_enrolled, is_enrollment_open: runMode.is_enrollment_open, key: this.context.key, marketing_url: runMode.marketing_url || '', + is_course_ended: runMode.is_course_ended, mode_slug: runMode.mode_slug, run_key: runMode.run_key, - start_date: runMode.start_date + enrollment_open_date: runMode.enrollment_open_date || '', + enrollable_run_modes: this.getEnrollableRunModes() }); } }, + setUnselected: function(){ + //This should be called to reset the model + //back to the unselected state + var unselectedMode = this.getUnselectedRunMode( + this.get('enrollable_run_modes')); + this.setActiveRunMode(unselectedMode); + }, + updateRun: function(runKey){ var selectedRun = _.findWhere(this.get('run_modes'), {run_key: runKey}); if (selectedRun){ diff --git a/lms/static/js/learner_dashboard/models/course_enroll_model.js b/lms/static/js/learner_dashboard/models/course_enroll_model.js index 5a2a26aecb..1977eb9767 100644 --- a/lms/static/js/learner_dashboard/models/course_enroll_model.js +++ b/lms/static/js/learner_dashboard/models/course_enroll_model.js @@ -11,7 +11,7 @@ return Backbone.Model.extend({ defaults: { course_id: '', - optIn: false, + optIn: false } }); } diff --git a/lms/static/js/learner_dashboard/views/course_enroll_view.js b/lms/static/js/learner_dashboard/views/course_enroll_view.js index f7d61e9e74..2e56ca6306 100644 --- a/lms/static/js/learner_dashboard/views/course_enroll_view.js +++ b/lms/static/js/learner_dashboard/views/course_enroll_view.js @@ -29,17 +29,14 @@ this.enrollModel = options.enrollModel; this.urlModel = options.urlModel; this.render(); - if(this.urlModel){ + if (this.urlModel){ this.trackSelectionUrl = this.urlModel.get('track_selection_url'); } }, render: function() { var filledTemplate; - if (this.$parentEl && - this.enrollModel && - this.model.get('course_key')){ - + if (this.$parentEl && this.enrollModel){ filledTemplate = this.tpl(this.model.toJSON()); HtmlUtils.setHtml(this.$el, filledTemplate); HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el)); @@ -48,7 +45,9 @@ handleEnroll: function(){ //Enrollment click event handled here - if (!this.model.get('is_enrolled')){ + if (!this.model.get('course_key')){ + this.$('.select-error').css('visibility','visible'); + } else if (!this.model.get('is_enrolled')){ // actually enroll this.enrollModel.save({ course_id: this.model.get('course_key') @@ -65,6 +64,9 @@ runKey = $(event.target).val(); if (runKey){ this.model.updateRun(runKey); + } else { + //Set back the unselected states + this.model.setUnselected(); } } }, @@ -74,7 +76,7 @@ if (this.trackSelectionUrl) { // Go to track selection page this.redirect( this.trackSelectionUrl + courseKey ); - }else{ + } else { this.model.set({ is_enrolled: true }); diff --git a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js index 6869bbe266..8ddb8c5689 100644 --- a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js @@ -20,7 +20,7 @@ define([ }, run_modes: [{ start_date: 'Apr 25, 2016', - end_date: 'Jun 13, 2016', + end_date: 'Jun 13, 2019', course_key: 'course-v1:ANUx+ANU-ASTRO1x+3T2015', course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info', marketing_url: 'https://www.edx.org/course/astrophysics-exploring', @@ -29,12 +29,15 @@ define([ run_key: '2T2016', course_started: true, is_enrolled: true, - certificate_url: '' + is_course_ended: false, + is_enrollment_open: true, + certificate_url: '', + enrollment_open_date: 'Mar 03, 2016' }] }, setupView = function(data, isEnrolled){ - context.run_modes[0].is_enrolled = isEnrolled; + data.run_modes[0].is_enrolled = isEnrolled; setFixtures('
'); courseCardModel = new CourseCardModel(data); view = new CourseCardView({ @@ -94,6 +97,28 @@ define([ expect(view.$('.certificate-status').length).toEqual(1); expect(view.$('.certificate-status .cta-secondary').attr('href')).toEqual(certUrl); }); + + it('should render the course card with coming soon', function(){ + view.remove(); + context.run_modes[0].is_enrollment_open = false; + setupView(context, false); + expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url); + expect(view.$('.course-details .course-title').text().trim()).toEqual(context.display_name); + expect(view.$('.course-details .course-title-link').length).toBe(0); + expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); + expect(view.$('.course-details .course-text .run-period').length).toBe(0); + expect(view.$('.no-action-message').text().trim()).toBe('Coming Soon'); + expect(view.$('.enroll-open-date').text().trim()) + .toBe(context.run_modes[0].enrollment_open_date); + }); + + it('should render if enrollment_open_date is not provided', function(){ + view.remove(); + context.run_modes[0].is_enrollment_open = true; + delete context.run_modes[0].enrollment_open_date; + setupView(context, false); + validateCourseInfoDisplay(); + }); }); } ); diff --git a/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js b/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js index a618e4b029..4ac1d58eb1 100644 --- a/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/course_enroll_view_spec.js @@ -21,9 +21,11 @@ define([ course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info', course_image_url: 'http://test.com/image1', marketing_url: 'http://test.com/image2', + is_course_ended: false, mode_slug: 'audit', run_key: '2T2016', - is_enrolled: false + is_enrolled: false, + is_enrollment_open: true }], multiRunModeList = [{ start_date: 'May 21, 2015', @@ -33,8 +35,10 @@ define([ course_image_url: 'http://test.com/run_2_image_1', marketing_url: 'http://test.com/run_2_image_2', mode_slug: 'verified', + is_course_ended: false, run_key: '1T2015', - is_enrolled: false + is_enrolled: false, + is_enrollment_open: true, },{ start_date: 'Sep 22, 2015', end_date: 'Dec 28, 2015', @@ -42,9 +46,11 @@ define([ course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info', course_image_url: 'http://test.com/run_3_image_1', marketing_url: 'http://test.com/run_3_image_2', + is_course_ended: false, mode_slug: 'verified', run_key: '2T2015', - is_enrolled: false + is_enrolled: false, + is_enrollment_open: true }], context = { display_name: 'Edx Demo course', @@ -117,13 +123,14 @@ define([ it('should render run selection drop down if mulitple run available', function(){ setupView(multiRunModeList); expect(view.$('.run-select').length).toBe(1); - expect(view.$('.run-select').val()).toEqual(multiRunModeList[0].run_key); + expect(view.$('.run-select').val()).toEqual(''); + expect(view.$('.run-select option').length).toBe(3); }); it('should switch run context if dropdown selection changed', function(){ setupView(multiRunModeList); spyOn(courseCardModel, 'updateRun').and.callThrough(); - expect(view.$('.run-select').val()).toEqual(multiRunModeList[0].run_key); + expect(view.$('.run-select').val()).toEqual(''); view.$('.run-select').val(multiRunModeList[1].run_key); view.$('.run-select').trigger("change"); expect(view.$('.run-select').val()).toEqual(multiRunModeList[1].run_key); diff --git a/lms/static/sass/elements/_course-card.scss b/lms/static/sass/elements/_course-card.scss index c1f1ea249b..2c76983ba0 100644 --- a/lms/static/sass/elements/_course-card.scss +++ b/lms/static/sass/elements/_course-card.scss @@ -11,7 +11,7 @@ padding: $baseline/2 $baseline; } - .course-image-link { + .course-image-container{ @include float(left); .header-img { @@ -47,8 +47,26 @@ margin-bottom: $baseline/2; text-transform: uppercase; } - - .run-select-container { + .select-error{ + color: palette(error, base); + margin-bottom: $baseline/4; + font-size: font-size(small); + visibility: hidden; + } + .no-action-message{ + margin-bottom: $baseline/2; + color: palette(grayscale, black); + font-size: font-size(large); + text-align: center; + } + .enrollment-opens{ + text-align: center; + margin-bottom: $baseline/2; + } + .enroll-open-date{ + text-align: center; + } + .run-select-container{ margin-bottom: $baseline; .run-select { @@ -61,8 +79,8 @@ text-align: center; } - .view-course-link { - width: $baseline*10; + .view-course-link{ + min-width: $baseline*10; text-align: center; } } diff --git a/lms/templates/learner_dashboard/course_card.underscore b/lms/templates/learner_dashboard/course_card.underscore index c6e6edb40e..b0b794a23f 100644 --- a/lms/templates/learner_dashboard/course_card.underscore +++ b/lms/templates/learner_dashboard/course_card.underscore @@ -1,20 +1,36 @@
- - <%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: display_name}, true) %> - +
+ <% if (course_url){ %> + + <%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: display_name}, true) %> + + <% } else { %> + + <% } %> + +

- + <% if (course_url){ %> + + <%- display_name %> + + <% }else{ %> <%- display_name %> - + <% } %>

- <%- start_date %> - <%- end_date %> - - + <% if (start_date && end_date){ %> + <%- start_date %> - <%- end_date %> + - + <% } %> <%- key %>
diff --git a/lms/templates/learner_dashboard/course_enroll.underscore b/lms/templates/learner_dashboard/course_enroll.underscore index 4b3b43ff60..aa0de0e259 100644 --- a/lms/templates/learner_dashboard/course_enroll.underscore +++ b/lms/templates/learner_dashboard/course_enroll.underscore @@ -1,32 +1,58 @@ <% if (is_enrolled){ %>
<%- gettext('enrolled') %>
- - <%- gettext('View Course') %> - + <% if (is_enrollment_open || is_course_ended){ %> + + <% if (is_enrollment_open){ %> + <%- gettext('View Course') %> + <% } else if (course_ended){ %> + <%- gettext('View Archived Course') %> + <% } %> + + <% } %> <% }else{ %> -
<%- gettext('not enrolled') %>
- <% if (run_modes.length > 1){ %> -
- - + - <% }); %> - + <% _.each (enrollable_run_modes, function(runMode){ %> + + <% }); %> + +
+ <% } %> + + <% } else {%> +
+ <%- gettext('Coming Soon') %> +
+
+ <%- gettext('Enrollment Opens') %> +
+
+ <%- enrollment_open_date %>
<% } %> - <% } %> diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 53e0743ef4..9dc0945a73 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -702,7 +702,6 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): def _assert_supplemented(self, actual, **kwargs): """DRY helper used to verify that program data is extended correctly.""" course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member - run_mode = dict( factories.RunMode( course_key=unicode(self.course.id), # pylint: disable=no-member @@ -710,6 +709,7 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): course_image_url=course_overview.course_image_url, start_date=self.course.start.strftime(self.human_friendly_format), end_date=self.course.end.strftime(self.human_friendly_format), + is_course_ended=self.course.end < timezone.now(), is_enrolled=False, is_enrollment_open=True, marketing_url='', @@ -745,7 +745,15 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): data = utils.supplement_program_data(self.program, self.user) - self._assert_supplemented(data, is_enrollment_open=is_enrollment_open) + if is_enrollment_open: + self._assert_supplemented( + data, + is_enrollment_open=is_enrollment_open) + else: + self._assert_supplemented( + data, + is_enrollment_open=is_enrollment_open, + enrollment_open_date=self.course.enrollment_start.strftime(self.human_friendly_format)) @ddt.data(True, False) @mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status') @@ -792,3 +800,11 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): mock_get_organization_by_short_name.return_value = {'logo': None} data = utils.supplement_program_data(self.program, self.user) self.assertEqual(data['organizations'][0].get('img'), None) + + @ddt.data(-1, 0, 1) + def test_course_course_ended(self, days_offset): + self.course.end = timezone.now() + datetime.timedelta(days=days_offset) + self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member + data = utils.supplement_program_data(self.program, self.user) + + self._assert_supplemented(data) diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index 2df3873ab8..2220803d5d 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -348,6 +348,7 @@ def supplement_program_data(program_data, user): end_date = course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC) run_mode['start_date'] = start_date.strftime(human_friendly_format) run_mode['end_date'] = end_date.strftime(human_friendly_format) + run_mode['is_course_ended'] = end_date < timezone.now() run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(user, course_key) @@ -355,6 +356,9 @@ def supplement_program_data(program_data, user): enrollment_end = course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC) is_enrollment_open = enrollment_start <= timezone.now() < enrollment_end run_mode['is_enrollment_open'] = is_enrollment_open + if not is_enrollment_open: + # Only render this enrollment open date if the enrollment open is in the future + run_mode['enrollment_open_date'] = enrollment_start.strftime(human_friendly_format) # TODO: Currently unavailable on LMS. run_mode['marketing_url'] = ''