From 30f25105e93e527384bf4fe08083eeb0c8059aa2 Mon Sep 17 00:00:00 2001 From: Peter Fogg Date: Fri, 11 Dec 2015 17:10:49 -0500 Subject: [PATCH] Support tool for changing enrollments. Allows support staff or global staff to view a list of a learner's enrollments, and change enrollment modes. We generate a ManualEnrollmentAudit record for these enrollment changes in order to track updates. Additionally, enrollment changes are handled through the enrollment API, which handles bookkeeping such as granting refunds where appropriate. ECOM-2825 --- common/djangoapps/student/models.py | 2 +- lms/djangoapps/support/serializers.py | 15 ++ .../support/js/collections/enrollment.js | 18 ++ .../static/support/js/enrollment_factory.js | 13 ++ .../static/support/js/models/enrollment.js | 24 +++ .../js/spec/collections/enrollment_spec.js | 22 +++ .../support/js/spec/models/enrollment_spec.js | 44 +++++ .../js/spec/{ => views}/certificates_spec.js | 0 .../js/spec/views/enrollment_modal_spec.js | 108 ++++++++++++ .../support/js/spec/views/enrollment_spec.js | 62 +++++++ .../js/spec_helpers/enrollment_helpers.js | 46 +++++ .../static/support/js/views/enrollment.js | 105 ++++++++++++ .../support/js/views/enrollment_modal.js | 76 +++++++++ .../templates/enrollment-modal.underscore | 26 +++ .../support/templates/enrollment.underscore | 62 +++++++ lms/djangoapps/support/tests/test_views.py | 160 ++++++++++++++++-- lms/djangoapps/support/urls.py | 2 + lms/djangoapps/support/views/__init__.py | 1 + lms/djangoapps/support/views/enrollments.py | 145 ++++++++++++++++ lms/djangoapps/support/views/index.py | 5 + lms/static/js/spec/main.js | 6 +- lms/static/lms/js/build.js | 3 +- lms/static/sass/views/_support.scss | 95 ++++++++++- lms/templates/support/enrollment.html | 30 ++++ 24 files changed, 1053 insertions(+), 17 deletions(-) create mode 100644 lms/djangoapps/support/serializers.py create mode 100644 lms/djangoapps/support/static/support/js/collections/enrollment.js create mode 100644 lms/djangoapps/support/static/support/js/enrollment_factory.js create mode 100644 lms/djangoapps/support/static/support/js/models/enrollment.js create mode 100644 lms/djangoapps/support/static/support/js/spec/collections/enrollment_spec.js create mode 100644 lms/djangoapps/support/static/support/js/spec/models/enrollment_spec.js rename lms/djangoapps/support/static/support/js/spec/{ => views}/certificates_spec.js (100%) create mode 100644 lms/djangoapps/support/static/support/js/spec/views/enrollment_modal_spec.js create mode 100644 lms/djangoapps/support/static/support/js/spec/views/enrollment_spec.js create mode 100644 lms/djangoapps/support/static/support/js/spec_helpers/enrollment_helpers.js create mode 100644 lms/djangoapps/support/static/support/js/views/enrollment.js create mode 100644 lms/djangoapps/support/static/support/js/views/enrollment_modal.js create mode 100644 lms/djangoapps/support/static/support/templates/enrollment-modal.underscore create mode 100644 lms/djangoapps/support/static/support/templates/enrollment.underscore create mode 100644 lms/djangoapps/support/views/enrollments.py create mode 100644 lms/templates/support/enrollment.html diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index d3976288a4..b2fd02a7ff 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -1489,7 +1489,7 @@ class ManualEnrollmentAudit(models.Model): """ saves the student manual enrollment information """ - cls.objects.create( + return cls.objects.create( enrolled_by=user, enrolled_email=email, state_transition=state_transition, diff --git a/lms/djangoapps/support/serializers.py b/lms/djangoapps/support/serializers.py new file mode 100644 index 0000000000..2e117e71a1 --- /dev/null +++ b/lms/djangoapps/support/serializers.py @@ -0,0 +1,15 @@ +""" +Serializers for use in the support app. +""" +from rest_framework import serializers + +from student.models import ManualEnrollmentAudit + + +class ManualEnrollmentSerializer(serializers.ModelSerializer): + """Serializes a manual enrollment audit object.""" + enrolled_by = serializers.SlugRelatedField(slug_field='email', read_only=True, default='') + + class Meta(object): + model = ManualEnrollmentAudit + fields = ('enrolled_by', 'time_stamp', 'reason') diff --git a/lms/djangoapps/support/static/support/js/collections/enrollment.js b/lms/djangoapps/support/static/support/js/collections/enrollment.js new file mode 100644 index 0000000000..5a8491a02b --- /dev/null +++ b/lms/djangoapps/support/static/support/js/collections/enrollment.js @@ -0,0 +1,18 @@ +;(function (define) { + 'use strict'; + define(['backbone', 'support/js/models/enrollment'], + function(Backbone, EnrollmentModel) { + return Backbone.Collection.extend({ + model: EnrollmentModel, + + initialize: function(models, options) { + this.user = options.user || ''; + this.baseUrl = options.baseUrl; + }, + + url: function() { + return this.baseUrl + this.user; + } + }); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/djangoapps/support/static/support/js/enrollment_factory.js b/lms/djangoapps/support/static/support/js/enrollment_factory.js new file mode 100644 index 0000000000..9e285b6b3b --- /dev/null +++ b/lms/djangoapps/support/static/support/js/enrollment_factory.js @@ -0,0 +1,13 @@ +;(function (define) { + 'use strict'; + + define([ + 'underscore', + 'support/js/views/enrollment' + ], function (_, EnrollmentView) { + return function (options) { + options = _.extend({el: '.enrollment-content'}, options); + return new EnrollmentView(options).render(); + }; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/djangoapps/support/static/support/js/models/enrollment.js b/lms/djangoapps/support/static/support/js/models/enrollment.js new file mode 100644 index 0000000000..1fb8ead6a8 --- /dev/null +++ b/lms/djangoapps/support/static/support/js/models/enrollment.js @@ -0,0 +1,24 @@ +(function (define) { + 'use strict'; + define(['backbone', 'underscore'], function (Backbone, _) { + return Backbone.Model.extend({ + updateEnrollment: function (new_mode, reason) { + return $.ajax({ + url: this.url(), + type: 'POST', + contentType: 'application/json', + data: JSON.stringify({ + course_id: this.get('course_id'), + new_mode: new_mode, + old_mode: this.get('mode'), + reason: reason + }), + success: _.bind(function (response) { + this.set('manual_enrollment', response); + this.set('mode', new_mode); + }, this) + }); + } + }); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/djangoapps/support/static/support/js/spec/collections/enrollment_spec.js b/lms/djangoapps/support/static/support/js/spec/collections/enrollment_spec.js new file mode 100644 index 0000000000..92435ce6a6 --- /dev/null +++ b/lms/djangoapps/support/static/support/js/spec/collections/enrollment_spec.js @@ -0,0 +1,22 @@ +define([ + 'common/js/spec_helpers/ajax_helpers', + 'support/js/spec_helpers/enrollment_helpers', + 'support/js/collections/enrollment', +], function (AjaxHelpers, EnrollmentHelpers, EnrollmentCollection) { + 'use strict'; + + describe('EnrollmentCollection', function () { + var enrollmentCollection; + + beforeEach(function () { + enrollmentCollection = new EnrollmentCollection([EnrollmentHelpers.mockEnrollmentData], { + user: 'test-user', + baseUrl: '/support/enrollment/' + }); + }); + + it('sets its URL based on the user', function () { + expect(enrollmentCollection.url()).toEqual('/support/enrollment/test-user'); + }); + }); +}); diff --git a/lms/djangoapps/support/static/support/js/spec/models/enrollment_spec.js b/lms/djangoapps/support/static/support/js/spec/models/enrollment_spec.js new file mode 100644 index 0000000000..1e18136918 --- /dev/null +++ b/lms/djangoapps/support/static/support/js/spec/models/enrollment_spec.js @@ -0,0 +1,44 @@ +define([ + 'common/js/spec_helpers/ajax_helpers', + 'support/js/spec_helpers/enrollment_helpers', + 'support/js/models/enrollment' +], function (AjaxHelpers, EnrollmentHelpers, EnrollmentModel) { + 'use strict'; + + describe('EnrollmentModel', function () { + var enrollment; + + beforeEach(function () { + enrollment = new EnrollmentModel(EnrollmentHelpers.mockEnrollmentData); + enrollment.url = function () { + return '/support/enrollment/test-user'; + }; + }); + + it('can save an enrollment to the server and updates itself on success', function () { + var requests = AjaxHelpers.requests(this), + manual_enrollment = { + 'enrolled_by': 'staff@edx.org', + 'reason': 'Financial Assistance' + }; + enrollment.updateEnrollment('verified', 'Financial Assistance'); + AjaxHelpers.expectJsonRequest(requests, 'POST', '/support/enrollment/test-user', { + course_id: EnrollmentHelpers.TEST_COURSE, + new_mode: 'verified', + old_mode: 'audit', + reason: 'Financial Assistance' + }); + AjaxHelpers.respondWithJson(requests, manual_enrollment); + expect(enrollment.get('mode')).toEqual('verified'); + expect(enrollment.get('manual_enrollment')).toEqual(manual_enrollment); + }); + + it('does not update itself on a server error', function () { + var requests = AjaxHelpers.requests(this); + enrollment.updateEnrollment('verified', 'Financial Assistance'); + AjaxHelpers.respondWithError(requests, 500); + expect(enrollment.get('mode')).toEqual('audit'); + expect(enrollment.get('manual_enrollment')).toEqual({}); + }); + }); +}); diff --git a/lms/djangoapps/support/static/support/js/spec/certificates_spec.js b/lms/djangoapps/support/static/support/js/spec/views/certificates_spec.js similarity index 100% rename from lms/djangoapps/support/static/support/js/spec/certificates_spec.js rename to lms/djangoapps/support/static/support/js/spec/views/certificates_spec.js diff --git a/lms/djangoapps/support/static/support/js/spec/views/enrollment_modal_spec.js b/lms/djangoapps/support/static/support/js/spec/views/enrollment_modal_spec.js new file mode 100644 index 0000000000..66c0b39221 --- /dev/null +++ b/lms/djangoapps/support/static/support/js/spec/views/enrollment_modal_spec.js @@ -0,0 +1,108 @@ +define([ + 'underscore', + 'common/js/spec_helpers/ajax_helpers', + 'support/js/spec_helpers/enrollment_helpers', + 'support/js/models/enrollment', + 'support/js/views/enrollment_modal' +], function (_, AjaxHelpers, EnrollmentHelpers, EnrollmentModel, EnrollmentModal) { + 'use strict'; + + describe('EnrollmentModal', function () { + + var modal; + + beforeEach(function () { + var enrollment = new EnrollmentModel(EnrollmentHelpers.mockEnrollmentData); + enrollment.url = function () { + return '/support/enrollment/test-user'; + }; + setFixtures(''); + modal = new EnrollmentModal({ + el: $('.enrollment-modal-wrapper'), + enrollment: enrollment, + modes: ['verified', 'audit'], + reasons: _.reduce( + ['Financial Assistance', 'Stampeding Buffalo', 'Angry Customer'], + function (acc, x) { acc[x] = x; return acc; }, + {} + ) + }).render(); + }); + + it('can render itself', function () { + expect($('.enrollment-modal h1').text()).toContain( + 'Change enrollment for ' + EnrollmentHelpers.TEST_COURSE + ); + expect($('.enrollment-change-field p').first().text()).toContain('Current enrollment mode: audit'); + + _.each(['verified', 'audit'], function (mode) { + expect($('.enrollment-new-mode').html()).toContain('