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('