From b0877270bfdf708bb4a5362d7dcdecd7ff8497e4 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Tue, 17 Oct 2017 10:43:41 -0400 Subject: [PATCH] Add course grades to programs dashboard. --- .../views/course_card_view.js | 5 + .../views/course_enroll_view.js | 9 +- .../views/program_details_view.js | 8 +- .../course_card_view_spec.js | 31 +++++- .../program_details_view_spec.js | 5 +- lms/static/sass/views/_program-details.scss | 13 +++ .../course_enroll.underscore | 7 +- .../djangoapps/programs/tests/factories.py | 1 + .../djangoapps/programs/tests/test_utils.py | 101 +++++++++++++++--- openedx/core/djangoapps/programs/utils.py | 12 ++- 10 files changed, 169 insertions(+), 23 deletions(-) diff --git a/lms/static/js/learner_dashboard/views/course_card_view.js b/lms/static/js/learner_dashboard/views/course_card_view.js index db7cf04bc2..399b829933 100644 --- a/lms/static/js/learner_dashboard/views/course_card_view.js +++ b/lms/static/js/learner_dashboard/views/course_card_view.js @@ -38,6 +38,9 @@ this.enrollModel.urlRoot = this.urlModel.get('commerce_api_url'); } this.context = options.context || {}; + this.grade = this.context.courseData.grades[this.model.get('course_run_key')]; + this.grade = this.grade * 100; + this.collectionCourseStatus = this.context.collectionCourseStatus || ''; this.render(); this.listenTo(this.model, 'change', this.render); }, @@ -59,6 +62,8 @@ this.enrollView = new CourseEnrollView({ $parentEl: this.$('.course-actions'), model: this.model, + grade: this.grade, + collectionCourseStatus: this.collectionCourseStatus, urlModel: this.urlModel, enrollModel: this.enrollModel }); 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 606b85e75f..00725d3b10 100644 --- a/lms/static/js/learner_dashboard/views/course_enroll_view.js +++ b/lms/static/js/learner_dashboard/views/course_enroll_view.js @@ -29,13 +29,18 @@ this.$parentEl = options.$parentEl; this.enrollModel = options.enrollModel; this.urlModel = options.urlModel; + this.grade = options.grade; + this.collectionCourseStatus = options.collectionCourseStatus; this.render(); }, render: function() { - var filledTemplate; + var filledTemplate, + context = this.model.toJSON(); if (this.$parentEl && this.enrollModel) { - filledTemplate = this.tpl(this.model.toJSON()); + context.grade = this.grade; + context.collectionCourseStatus = this.collectionCourseStatus; + filledTemplate = this.tpl(context); HtmlUtils.setHtml(this.$el, filledTemplate); HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el)); } diff --git a/lms/static/js/learner_dashboard/views/program_details_view.js b/lms/static/js/learner_dashboard/views/program_details_view.js index 7230720988..18f1c108ba 100644 --- a/lms/static/js/learner_dashboard/views/program_details_view.js +++ b/lms/static/js/learner_dashboard/views/program_details_view.js @@ -91,7 +91,7 @@ el: '.js-course-list-remaining', childView: CourseCardView, collection: this.remainingCourseCollection, - context: this.options + context: $.extend(this.options, {collectionCourseStatus: 'remaining'}) }).render(); } @@ -100,7 +100,7 @@ el: '.js-course-list-completed', childView: CourseCardView, collection: this.completedCourseCollection, - context: this.options + context: $.extend(this.options, {collectionCourseStatus: 'completed'}) }).render(); } @@ -110,7 +110,9 @@ el: '.js-course-list-in-progress', childView: CourseCardView, collection: this.inProgressCourseCollection, - context: $.extend(this.options, {enrolled: gettext('Enrolled')}) + context: $.extend(this.options, + {enrolled: gettext('Enrolled'), collectionCourseStatus: 'in_progress'} + ) }).render(); } 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 425d67d3e5..2ef08531dd 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 @@ -13,14 +13,27 @@ define([ startDate = 'Feb 28, 2017', endDate = 'May 30, 2017', - setupView = function(data, isEnrolled) { - var programData = $.extend({}, data); + setupView = function(data, isEnrolled, collectionCourseStatus) { + var programData = $.extend({}, data), + context = { + courseData: { + grades: { + 'course-v1:WageningenX+FFESx+1T2017': 0.8 + } + }, + collectionCourseStatus: collectionCourseStatus + }; + + if (typeof collectionCourseStatus === 'undefined') { + context.collectionCourseStatus = 'completed'; + } programData.course_runs[0].is_enrolled = isEnrolled; setFixtures('
'); courseCardModel = new CourseCardModel(programData); view = new CourseCardView({ - model: courseCardModel + model: courseCardModel, + context: context }); }, @@ -84,6 +97,18 @@ define([ validateCourseInfoDisplay(); }); + it('should render final grade if course is completed', function() { + view.remove(); + setupView(course, true); + expect(view.$('.grade-display').text()).toEqual('80%'); + }); + + it('should not render final grade if course has not been completed', function() { + view.remove(); + setupView(course, true, 'in_progress'); + expect(view.$('.final-grade').length).toEqual(0); + }); + it('should render the course card based on the data not enrolled', function() { validateCourseInfoDisplay(); }); diff --git a/lms/static/js/spec/learner_dashboard/program_details_view_spec.js b/lms/static/js/spec/learner_dashboard/program_details_view_spec.js index 57f0670504..290f7db864 100644 --- a/lms/static/js/spec/learner_dashboard/program_details_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/program_details_view_spec.js @@ -462,7 +462,10 @@ define([ } ] } - ] + ], + grades: { + 'course-v1:Testx+DOGx002+1T2016': 0.9 + } }, urls: { program_listing_url: '/dashboard/programs/', diff --git a/lms/static/sass/views/_program-details.scss b/lms/static/sass/views/_program-details.scss index 46da9634bd..b30b35006e 100644 --- a/lms/static/sass/views/_program-details.scss +++ b/lms/static/sass/views/_program-details.scss @@ -520,6 +520,19 @@ width: 100%; } + + .final-grade { + .grade-header { + color: palette(grayscale, base); + font-weight: bold; + } + .grade-display { + padding-right: 15px; + font-size: 1.5em; + color: palette(primary, accent); + } + } + .upgrade-message { flex-wrap: wrap; diff --git a/lms/templates/learner_dashboard/course_enroll.underscore b/lms/templates/learner_dashboard/course_enroll.underscore index b6769ce007..5e6dd72d11 100644 --- a/lms/templates/learner_dashboard/course_enroll.underscore +++ b/lms/templates/learner_dashboard/course_enroll.underscore @@ -1,4 +1,9 @@ -<% if (is_enrolled && (typeof expired === 'undefined' || expired === false)) { %> +<% if (is_enrolled && collectionCourseStatus === 'completed') { %> +
+
<%- gettext('Final Grade') %><%- StringUtils.interpolate(gettext('for {courseName}'), {courseName: title}) %>
+
<%- grade %>%
+
+<% } else if (is_enrolled && (typeof expired === 'undefined' || expired === false)) { %> <% if (is_course_ended) { %> <%- gettext('View Archived Course') %> diff --git a/openedx/core/djangoapps/programs/tests/factories.py b/openedx/core/djangoapps/programs/tests/factories.py index eb81dda41a..280e05962f 100644 --- a/openedx/core/djangoapps/programs/tests/factories.py +++ b/openedx/core/djangoapps/programs/tests/factories.py @@ -12,3 +12,4 @@ class ProgressFactory(factory.Factory): completed = 0 in_progress = 0 not_started = 0 + grades = dict() diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 4747764607..d602afcbb4 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -18,6 +18,7 @@ from course_modes.models import CourseMode from lms.djangoapps.certificates.api import MODES from lms.djangoapps.commerce.tests.test_utils import update_commerce_config from lms.djangoapps.commerce.utils import EcommerceService +from lms.djangoapps.grades.tests.utils import mock_passing_grade from openedx.core.djangoapps.catalog.tests.factories import ( generate_course_run_key, ProgramFactory, @@ -138,7 +139,7 @@ class TestProgramProgressMeter(TestCase): self.assertEqual(meter.engaged_programs, [program]) self._assert_progress( meter, - ProgressFactory(uuid=program['uuid'], in_progress=1) + ProgressFactory(uuid=program['uuid'], in_progress=1, grades={course_run_key: 0.0}) ) self.assertEqual(meter.completed_programs, []) @@ -169,7 +170,8 @@ class TestProgramProgressMeter(TestCase): uuid=program['uuid'], completed=[], in_progress=[program['courses'][0]], - not_started=[] + not_started=[], + grades={course_run_key: 0.0}, ) ] @@ -205,7 +207,8 @@ class TestProgramProgressMeter(TestCase): uuid=program['uuid'], completed=[], in_progress=[program['courses'][0]], - not_started=[] + not_started=[], + grades={course_run_key: 0.0}, ) ] @@ -246,7 +249,8 @@ class TestProgramProgressMeter(TestCase): uuid=program['uuid'], completed=0, in_progress=1 if offset in [None, 1] else 0, - not_started=1 if offset in [-1] else 0 + not_started=1 if offset in [-1] else 0, + grades={course_run_key: 0.0}, ) ] @@ -285,9 +289,14 @@ class TestProgramProgressMeter(TestCase): self._attach_detail_url(data) programs = data[:2] self.assertEqual(meter.engaged_programs, programs) + + grades = { + newer_course_run_key: 0.0, + older_course_run_key: 0.0, + } self._assert_progress( meter, - *(ProgressFactory(uuid=program['uuid'], in_progress=1) for program in programs) + *(ProgressFactory(uuid=program['uuid'], in_progress=1, grades=grades) for program in programs) ) self.assertEqual(meter.completed_programs, []) @@ -330,9 +339,15 @@ class TestProgramProgressMeter(TestCase): self._attach_detail_url(data) programs = data[:3] self.assertEqual(meter.engaged_programs, programs) + + grades = { + solo_course_run_key: 0.0, + shared_course_run_key: 0.0, + } + self._assert_progress( meter, - *(ProgressFactory(uuid=program['uuid'], in_progress=1) for program in programs) + *(ProgressFactory(uuid=program['uuid'], in_progress=1, grades=grades) for program in programs) ) self.assertEqual(meter.completed_programs, []) @@ -366,7 +381,7 @@ class TestProgramProgressMeter(TestCase): program, program_uuid = data[0], data[0]['uuid'] self._assert_progress( meter, - ProgressFactory(uuid=program_uuid, in_progress=1, not_started=1) + ProgressFactory(uuid=program_uuid, in_progress=1, not_started=1, grades={first_course_run_key: 0.0}) ) self.assertEqual(meter.completed_programs, []) @@ -375,7 +390,14 @@ class TestProgramProgressMeter(TestCase): meter = ProgramProgressMeter(self.site, self.user) self._assert_progress( meter, - ProgressFactory(uuid=program_uuid, in_progress=2) + ProgressFactory( + uuid=program_uuid, + in_progress=2, + grades={ + first_course_run_key: 0.0, + second_course_run_key: 0.0, + }, + ) ) self.assertEqual(meter.completed_programs, []) @@ -386,7 +408,15 @@ class TestProgramProgressMeter(TestCase): meter = ProgramProgressMeter(self.site, self.user) self._assert_progress( meter, - ProgressFactory(uuid=program_uuid, completed=1, in_progress=1) + ProgressFactory( + uuid=program_uuid, + completed=1, + in_progress=1, + grades={ + first_course_run_key: 0.0, + second_course_run_key: 0.0, + } + ) ) self.assertEqual(meter.completed_programs, []) @@ -398,7 +428,15 @@ class TestProgramProgressMeter(TestCase): meter = ProgramProgressMeter(self.site, self.user) self._assert_progress( meter, - ProgressFactory(uuid=program_uuid, completed=1, in_progress=1) + ProgressFactory( + uuid=program_uuid, + completed=1, + in_progress=1, + grades={ + first_course_run_key: 0.0, + second_course_run_key: 0.0, + } + ) ) self.assertEqual(meter.completed_programs, []) @@ -410,7 +448,14 @@ class TestProgramProgressMeter(TestCase): meter = ProgramProgressMeter(self.site, self.user) self._assert_progress( meter, - ProgressFactory(uuid=program_uuid, completed=2) + ProgressFactory( + uuid=program_uuid, + completed=2, + grades={ + first_course_run_key: 0.0, + second_course_run_key: 0.0, + } + ) ) self.assertEqual(meter.completed_programs, [program_uuid]) @@ -443,7 +488,7 @@ class TestProgramProgressMeter(TestCase): program, program_uuid = data[0], data[0]['uuid'] self._assert_progress( meter, - ProgressFactory(uuid=program_uuid, completed=1) + ProgressFactory(uuid=program_uuid, completed=1, grades={course_run_key: 0.0}) ) self.assertEqual(meter.completed_programs, [program_uuid]) @@ -549,6 +594,38 @@ class TestProgramProgressMeter(TestCase): mock_completed_course_runs.return_value = [{'course_run_id': course_run_key, 'type': 'verified'}] self.assertEqual(meter._is_course_complete(course), True) + def test_course_grade_results(self, mock_get_programs): + grade_percent = .8 + with mock_passing_grade(percent=grade_percent): + course_run_key = generate_course_run_key() + data = [ + ProgramFactory( + courses=[ + CourseFactory(course_runs=[ + CourseRunFactory(key=course_run_key), + ]), + ] + ) + ] + mock_get_programs.return_value = data + + self._create_enrollments(course_run_key) + + meter = ProgramProgressMeter(self.site, self.user) + + program = data[0] + expected = [ + ProgressFactory( + uuid=program['uuid'], + completed=[], + in_progress=[program['courses'][0]], + not_started=[], + grades={course_run_key: grade_percent}, + ) + ] + + self.assertEqual(meter.progress(count_only=False), expected) + def _create_course(self, course_price, course_run_count=1): """ diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index 02b5bdbc95..15a4c1b01f 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -22,6 +22,8 @@ from course_modes.models import CourseMode from lms.djangoapps.certificates import api as certificate_api from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.courses import get_course_with_access +from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory from openedx.core.djangoapps.catalog.utils import get_programs from openedx.core.djangoapps.commerce.utils import ecommerce_api_client from openedx.core.djangoapps.content.course_overviews.models import CourseOverview @@ -89,6 +91,8 @@ class ProgramProgressMeter(object): # We can't use dict.keys() for this because the course run ids need to be ordered self.course_run_ids.append(enrollment_id) + self.course_grade_factory = CourseGradeFactory() + if uuid: self.programs = [get_programs(self.site, uuid=uuid)] else: @@ -216,11 +220,17 @@ class ProgramProgressMeter(object): else: not_started.append(course) + grades = {} + for run in self.course_run_ids: + grade = self.course_grade_factory.read(self.user, course_key=CourseKey.from_string(run)) + grades[run] = grade.percent + progress.append({ 'uuid': program_copy['uuid'], 'completed': len(completed) if count_only else completed, 'in_progress': len(in_progress) if count_only else in_progress, 'not_started': len(not_started) if count_only else not_started, + 'grades': grades, }) return progress @@ -325,7 +335,7 @@ class ProgramProgressMeter(object): course_data = { 'course_run_id': unicode(certificate['course_key']), - 'type': certificate_type + 'type': certificate_type, } if certificate_api.is_passing_status(certificate['status']):