diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 70686f0353..08666660f3 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -64,8 +64,6 @@ from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements from openedx.core.djangoapps.models.course_details import CourseDetails -from openedx.core.djangoapps.programs.models import ProgramsApiConfig -from openedx.core.djangoapps.programs.utils import get_programs from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.lib.course_tabs import CourseTabPluginManager @@ -469,13 +467,6 @@ def course_listing(request): courses, in_process_course_actions = get_courses_accessible_to_user(request) libraries = _accessible_libraries_list(request.user) if LIBRARIES_ENABLED else [] - programs_config = ProgramsApiConfig.current() - raw_programs = get_programs(request.user) if programs_config.is_studio_tab_enabled else [] - - # Sort programs alphabetically by name. - # TODO: Support ordering in the Programs API itself. - programs = sorted(raw_programs, key=lambda p: p['name'].lower()) - def format_in_process_course_view(uca): """ Return a dict of the data which the view requires for each unsucceeded course @@ -525,9 +516,6 @@ def course_listing(request): 'rerun_creator_status': GlobalStaff().has_user(request.user), 'allow_unicode_course_id': settings.FEATURES.get('ALLOW_UNICODE_COURSE_ID', False), 'allow_course_reruns': settings.FEATURES.get('ALLOW_COURSE_RERUNS', True), - 'is_programs_enabled': programs_config.is_studio_tab_enabled and request.user.is_staff, - 'programs': programs, - 'program_authoring_url': reverse('programs'), }) diff --git a/cms/djangoapps/contentstore/views/program.py b/cms/djangoapps/contentstore/views/program.py deleted file mode 100644 index 0ac51d5df0..0000000000 --- a/cms/djangoapps/contentstore/views/program.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Programs views for use with Studio.""" -from django.conf import settings -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import reverse -from django.http import Http404, JsonResponse -from django.utils.decorators import method_decorator -from django.views.generic import View -from provider.oauth2.models import Client - -from edxmako.shortcuts import render_to_response -from openedx.core.djangoapps.programs.models import ProgramsApiConfig -from openedx.core.lib.token_utils import JwtBuilder - - -class ProgramAuthoringView(View): - """View rendering a template which hosts the Programs authoring app. - - The Programs authoring app is a Backbone SPA. The app handles its own routing - and provides a UI which can be used to create and publish new Programs. - """ - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - """Populate the template context with values required for the authoring app to run.""" - programs_config = ProgramsApiConfig.current() - - if programs_config.is_studio_tab_enabled and request.user.is_staff: - return render_to_response('program_authoring.html', { - 'lms_base_url': '//{}/'.format(settings.LMS_BASE), - 'programs_api_url': programs_config.public_api_url, - 'programs_token_url': reverse('programs_id_token'), - 'studio_home_url': reverse('home'), - 'uses_pattern_library': True - }) - else: - raise Http404 - - -class ProgramsIdTokenView(View): - """Provides id tokens to JavaScript clients for use with the Programs API.""" - - @method_decorator(login_required) - def get(self, request, *args, **kwargs): - """Generate and return a token, if the integration is enabled.""" - if ProgramsApiConfig.current().is_studio_tab_enabled: - # TODO: Use the system's JWT_AUDIENCE and JWT_SECRET_KEY instead of client ID and name. - client_name = 'programs' - - try: - client = Client.objects.get(name=client_name) - except Client.DoesNotExist: - raise ImproperlyConfigured( - 'OAuth2 Client with name [{}] does not exist.'.format(client_name) - ) - - scopes = ['email', 'profile'] - expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION - jwt = JwtBuilder(request.user, secret=client.client_secret).build_token( - scopes, - expires_in, - aud=client.client_id - ) - - return JsonResponse({'id_token': jwt}) - else: - raise Http404 diff --git a/cms/djangoapps/contentstore/views/tests/test_programs.py b/cms/djangoapps/contentstore/views/tests/test_programs.py deleted file mode 100644 index 0d6dff1e29..0000000000 --- a/cms/djangoapps/contentstore/views/tests/test_programs.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Tests covering the Programs listing on the Studio home.""" -import json - -from django.conf import settings -from django.core.urlresolvers import reverse -import httpretty -import mock -from edx_oauth2_provider.tests.factories import ClientFactory -from provider.constants import CONFIDENTIAL - -from openedx.core.djangoapps.programs.models import ProgramsApiConfig -from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin -from openedx.core.djangolib.markup import Text -from student.tests.factories import UserFactory -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase - - -class TestProgramListing(ProgramsApiConfigMixin, ProgramsDataMixin, SharedModuleStoreTestCase): - """Verify Program listing behavior.""" - def setUp(self): - super(TestProgramListing, self).setUp() - - ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) - - self.staff = UserFactory(is_staff=True) - self.client.login(username=self.staff.username, password='test') - - self.studio_home = reverse('home') - - @httpretty.activate - def test_programs_config_disabled(self): - """Verify that the programs tab and creation button aren't rendered when config is disabled.""" - self.create_programs_config(enable_studio_tab=False) - self.mock_programs_api() - - response = self.client.get(self.studio_home) - - self.assertNotIn("You haven't created any programs yet.", response.content) - - for program_name in self.PROGRAM_NAMES: - self.assertNotIn(program_name, response.content) - - @httpretty.activate - def test_programs_requires_staff(self): - """ - Verify that the programs tab and creation button aren't rendered unless the user has - global staff permissions. - """ - student = UserFactory(is_staff=False) - self.client.login(username=student.username, password='test') - - self.create_programs_config() - self.mock_programs_api() - - response = self.client.get(self.studio_home) - self.assertNotIn("You haven't created any programs yet.", response.content) - - @httpretty.activate - def test_programs_displayed(self): - """Verify that the programs tab and creation button can be rendered when config is enabled.""" - - # When no data is provided, expect creation prompt. - self.create_programs_config() - self.mock_programs_api(data={'results': []}) - - response = self.client.get(self.studio_home) - self.assertIn(Text("You haven't created any programs yet."), response.content.decode('utf-8')) - - # When data is provided, expect a program listing. - self.mock_programs_api() - - response = self.client.get(self.studio_home) - for program_name in self.PROGRAM_NAMES: - self.assertIn(program_name, response.content) - - -class TestProgramAuthoringView(ProgramsApiConfigMixin, SharedModuleStoreTestCase): - """Verify the behavior of the program authoring app's host view.""" - def setUp(self): - super(TestProgramAuthoringView, self).setUp() - - self.staff = UserFactory(is_staff=True) - self.programs_path = reverse('programs') - - def _assert_status(self, status_code): - """Verify the status code returned by the Program authoring view.""" - response = self.client.get(self.programs_path) - self.assertEquals(response.status_code, status_code) - - return response - - def test_authoring_login_required(self): - """Verify that accessing the view requires the user to be authenticated.""" - response = self.client.get(self.programs_path) - self.assertRedirects( - response, - '{login_url}?next={programs}'.format( - login_url=settings.LOGIN_URL, - programs=self.programs_path - ) - ) - - def test_authoring_header(self): - """Verify that the header contains the expected text.""" - self.client.login(username=self.staff.username, password='test') - self.create_programs_config() - - response = self._assert_status(200) - self.assertIn("Program Administration", response.content) - - def test_authoring_access(self): - """ - Verify that a 404 is returned if Programs authoring is disabled, or the user does not have - global staff permissions. - """ - self.client.login(username=self.staff.username, password='test') - self._assert_status(404) - - # Enable Programs authoring interface - self.create_programs_config() - - student = UserFactory(is_staff=False) - self.client.login(username=student.username, password='test') - self._assert_status(404) - - -class TestProgramsIdTokenView(ProgramsApiConfigMixin, SharedModuleStoreTestCase): - """Tests for the programs id_token endpoint.""" - - def setUp(self): - super(TestProgramsIdTokenView, self).setUp() - self.user = UserFactory() - self.client.login(username=self.user.username, password='test') - self.path = reverse('programs_id_token') - - def test_config_disabled(self): - """Ensure the endpoint returns 404 when Programs authoring is disabled.""" - self.create_programs_config(enable_studio_tab=False) - response = self.client.get(self.path) - self.assertEqual(response.status_code, 404) - - def test_not_logged_in(self): - """Ensure the endpoint denies access to unauthenticated users.""" - self.create_programs_config() - self.client.logout() - response = self.client.get(self.path) - self.assertEqual(response.status_code, 302) - self.assertIn(settings.LOGIN_URL, response['Location']) - - @mock.patch('cms.djangoapps.contentstore.views.program.JwtBuilder.build_token') - def test_config_enabled(self, mock_build_token): - """ - Ensure the endpoint responds with a valid JSON payload when authoring - is enabled. - """ - mock_build_token.return_value = 'test-id-token' - ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) - - self.create_programs_config() - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - payload = json.loads(response.content) - self.assertEqual(payload, {'id_token': 'test-id-token'}) diff --git a/cms/envs/common.py b/cms/envs/common.py index d5cbeab985..b04b16af6a 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -908,9 +908,6 @@ INSTALLED_APPS = ( # Bookmarks 'openedx.core.djangoapps.bookmarks', - # programs support - 'openedx.core.djangoapps.programs', - # Catalog integration 'openedx.core.djangoapps.catalog', diff --git a/cms/static/cms/js/build.js b/cms/static/cms/js/build.js index e078c90603..9051eace83 100644 --- a/cms/static/cms/js/build.js +++ b/cms/static/cms/js/build.js @@ -53,8 +53,7 @@ 'js/factories/settings_graders', 'js/factories/textbooks', 'js/factories/videos_index', - 'js/factories/xblock_validation', - 'js/programs/program_admin_app' + 'js/factories/xblock_validation' ]), /** * By default all the configuration for optimization happens from the command diff --git a/cms/static/cms/js/require-config.js b/cms/static/cms/js/require-config.js index afa80d3c18..3c8e2fbb10 100644 --- a/cms/static/cms/js/require-config.js +++ b/cms/static/cms/js/require-config.js @@ -59,7 +59,6 @@ 'underscore.string': 'common/js/vendor/underscore.string', 'backbone': 'common/js/vendor/backbone', 'backbone-relational': 'js/vendor/backbone-relational.min', - 'backbone.validation': 'common/js/vendor/backbone-validation-min', 'backbone.associations': 'js/vendor/backbone-associations-min', 'backbone.paginator': 'common/js/vendor/backbone.paginator', 'tinymce': 'js/vendor/tinymce/js/tinymce/tinymce.full.min', diff --git a/cms/static/cms/js/spec/main.js b/cms/static/cms/js/spec/main.js index 8eee091914..45efca4dc2 100644 --- a/cms/static/cms/js/spec/main.js +++ b/cms/static/cms/js/spec/main.js @@ -56,7 +56,6 @@ 'backbone': 'common/js/vendor/backbone', 'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min', 'backbone.paginator': 'common/js/vendor/backbone.paginator', - 'backbone.validation': 'common/js/vendor/backbone-validation-min', 'backbone-relational': 'xmodule_js/common_static/js/vendor/backbone-relational.min', 'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min', 'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce', @@ -290,10 +289,7 @@ 'js/certificates/spec/views/certificate_details_spec', 'js/certificates/spec/views/certificate_editor_spec', 'js/certificates/spec/views/certificates_list_spec', - 'js/certificates/spec/views/certificate_preview_spec', - 'js/spec/models/auto_auth_model_spec', - 'js/spec/views/programs/program_creator_spec', - 'js/spec/views/programs/program_details_spec' + 'js/certificates/spec/views/certificate_preview_spec' ]; i = 0; diff --git a/cms/static/cms/js/spec/main_squire.js b/cms/static/cms/js/spec/main_squire.js index 34d25f40f8..879f0d3612 100644 --- a/cms/static/cms/js/spec/main_squire.js +++ b/cms/static/cms/js/spec/main_squire.js @@ -37,7 +37,6 @@ 'backbone': 'common/js/vendor/backbone', 'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min', 'backbone.paginator': 'common/js/vendor/backbone.paginator', - 'backbone.validation': 'common/js/vendor/backbone-validation', 'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min', 'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce', 'xmodule': 'xmodule_js/src/xmodule', diff --git a/cms/static/js/index.js b/cms/static/js/index.js index b0b1c8c673..ae88a24dcc 100644 --- a/cms/static/js/index.js +++ b/cms/static/js/index.js @@ -162,7 +162,6 @@ define(['domReady', 'jquery', 'underscore', 'js/utils/cancel_on_escape', 'js/vie e.preventDefault(); $('.courses-tab').toggleClass('active', tab === 'courses'); $('.libraries-tab').toggleClass('active', tab === 'libraries'); - $('.programs-tab').toggleClass('active', tab === 'programs'); // Also toggle this course-related notice shown below the course tab, if it is present: $('.wrapper-creationrights').toggleClass('is-hidden', tab !== 'courses'); @@ -181,7 +180,6 @@ define(['domReady', 'jquery', 'underscore', 'js/utils/cancel_on_escape', 'js/vie $('#course-index-tabs .courses-tab').bind('click', showTab('courses')); $('#course-index-tabs .libraries-tab').bind('click', showTab('libraries')); - $('#course-index-tabs .programs-tab').bind('click', showTab('programs')); }; domReady(onReady); diff --git a/cms/static/js/programs/collections/course_runs_collection.js b/cms/static/js/programs/collections/course_runs_collection.js deleted file mode 100644 index b95175e14d..0000000000 --- a/cms/static/js/programs/collections/course_runs_collection.js +++ /dev/null @@ -1,58 +0,0 @@ -define([ - 'backbone', - 'jquery', - 'js/programs/utils/api_config', - 'jquery.cookie' -], - function(Backbone, $, apiConfig) { - 'use strict'; - - return Backbone.Collection.extend({ - allRuns: [], - - initialize: function(models, options) { - // Ignore pagination and give me everything - var orgStr = options.organization.key, - queries = '?org=' + orgStr + '&page_size=1000'; - - this.url = apiConfig.get('lmsBaseUrl') + 'api/courses/v1/courses/' + queries; - }, - - /* - * Abridged version of Backbone.Collection.Create that does not - * save the updated Collection back to the server - * (code based on original function - http://backbonejs.org/docs/backbone.html#section-134) - */ - create: function(model, options) { - options = options ? _.clone(options) : {}; - model = this._prepareModel(model, options); - - if (!!model) { - this.add(model, options); - return model; - } - }, - - parse: function(data) { - this.allRuns = data.results; - - // Because pagination is ignored just set results - return data.results; - }, - - // Adds a run back into the model for selection - addRun: function(id) { - var courseRun = _.findWhere(this.allRuns, {id: id}); - - this.create(courseRun); - }, - - // Removes a run from the model for selection - removeRun: function(id) { - var courseRun = this.where({id: id}); - - this.remove(courseRun); - } - }); - } -); diff --git a/cms/static/js/programs/collections/programs_collection.js b/cms/static/js/programs/collections/programs_collection.js deleted file mode 100644 index 22c135c960..0000000000 --- a/cms/static/js/programs/collections/programs_collection.js +++ /dev/null @@ -1,13 +0,0 @@ -define([ - 'backbone', - 'jquery', - 'js/programs/models/program_model' -], - function(Backbone, $, ProgramModel) { - 'use strict'; - - return Backbone.Collection.extend({ - model: ProgramModel - }); - } -); diff --git a/cms/static/js/programs/models/api_config_model.js b/cms/static/js/programs/models/api_config_model.js deleted file mode 100644 index 0bab0612a4..0000000000 --- a/cms/static/js/programs/models/api_config_model.js +++ /dev/null @@ -1,17 +0,0 @@ -define([ - 'backbone' -], - function(Backbone) { - 'use strict'; - - return Backbone.Model.extend({ - defaults: { - username: '', - lmsBaseUrl: '', - programsApiUrl: '', - authUrl: '/programs/id_token/', - idToken: '' - } - }); - } -); diff --git a/cms/static/js/programs/models/auto_auth_model.js b/cms/static/js/programs/models/auto_auth_model.js deleted file mode 100644 index 1088ec84b8..0000000000 --- a/cms/static/js/programs/models/auto_auth_model.js +++ /dev/null @@ -1,10 +0,0 @@ -define([ - 'backbone', - 'js/programs/utils/auth_utils' -], - function(Backbone, auth) { - 'use strict'; - - return Backbone.Model.extend(auth.autoSync); - } -); diff --git a/cms/static/js/programs/models/course_model.js b/cms/static/js/programs/models/course_model.js deleted file mode 100644 index 6ba544be5c..0000000000 --- a/cms/static/js/programs/models/course_model.js +++ /dev/null @@ -1,38 +0,0 @@ -define([ - 'backbone', - 'jquery', - 'js/programs/utils/api_config', - 'js/programs/models/auto_auth_model', - 'jquery.cookie', - 'gettext' -], - function(Backbone, $, apiConfig, AutoAuthModel) { - 'use strict'; - - return AutoAuthModel.extend({ - - validation: { - key: { - required: true, - maxLength: 64 - }, - display_name: { - required: true, - maxLength: 128 - } - }, - - labels: { - key: gettext('Course Code'), - display_name: gettext('Course Title') - }, - - defaults: { - display_name: false, - key: false, - organization: [], - run_modes: [] - } - }); - } -); diff --git a/cms/static/js/programs/models/course_run_model.js b/cms/static/js/programs/models/course_run_model.js deleted file mode 100644 index 3c7fb568a4..0000000000 --- a/cms/static/js/programs/models/course_run_model.js +++ /dev/null @@ -1,17 +0,0 @@ -define([ - 'backbone' -], - function(Backbone) { - 'use strict'; - - return Backbone.Model.extend({ - defaults: { - course_key: '', - mode_slug: 'verified', - sku: '', - start_date: '', - run_key: '' - } - }); - } -); diff --git a/cms/static/js/programs/models/organizations_model.js b/cms/static/js/programs/models/organizations_model.js deleted file mode 100644 index 70748d6668..0000000000 --- a/cms/static/js/programs/models/organizations_model.js +++ /dev/null @@ -1,16 +0,0 @@ -define([ - 'js/programs/utils/api_config', - 'js/programs/models/auto_auth_model' -], - function(apiConfig, AutoAuthModel) { - 'use strict'; - - return AutoAuthModel.extend({ - - url: function() { - return apiConfig.get('programsApiUrl') + 'organizations/?page_size=1000'; - } - - }); - } -); diff --git a/cms/static/js/programs/models/program_model.js b/cms/static/js/programs/models/program_model.js deleted file mode 100644 index af0d097fda..0000000000 --- a/cms/static/js/programs/models/program_model.js +++ /dev/null @@ -1,107 +0,0 @@ -define([ - 'backbone', - 'jquery', - 'js/programs/utils/api_config', - 'js/programs/models/auto_auth_model', - 'gettext', - 'jquery.cookie' -], - function(Backbone, $, apiConfig, AutoAuthModel, gettext) { - 'use strict'; - - return AutoAuthModel.extend({ - - // Backbone.Validation rules. - // See: http://thedersen.com/projects/backbone-validation/#configure-validation-rules-on-the-model. - validation: { - name: { - required: true, - maxLength: 255 - }, - subtitle: { - // The underlying Django model does not require a subtitle. - maxLength: 255 - }, - category: { - required: true, - // TODO: Populate with the results of an API call for valid categories. - oneOf: ['XSeries', 'MicroMasters'] - }, - organizations: 'validateOrganizations', - marketing_slug: { - maxLength: 255 - } - }, - - initialize: function() { - this.url = apiConfig.get('programsApiUrl') + 'programs/' + this.id + '/'; - }, - - validateOrganizations: function(orgArray) { - /** - * The array passed to this method contains a single object representing - * the selected organization; the object contains the organization's key. - * In the future, multiple organizations might be associated with a program. - */ - var i, - len = orgArray ? orgArray.length : 0; - - for (i = 0; i < len; i++) { - if (orgArray[i].key === 'false') { - return gettext('Please select a valid organization.'); - } - } - }, - - getConfig: function(options) { - var patch = options && options.patch, - params = patch ? this.get('id') + '/' : '', - config = _.extend({validate: true, parse: true}, { - type: patch ? 'PATCH' : 'POST', - url: apiConfig.get('programsApiUrl') + 'programs/' + params, - contentType: patch ? 'application/merge-patch+json' : 'application/json', - context: this, - // NB: setting context fails in tests - success: _.bind(this.saveSuccess, this), - error: _.bind(this.saveError, this) - }); - - if (patch) { - config.data = JSON.stringify(options.update) || this.attributes; - } - - return config; - }, - - patch: function(data) { - this.save({ - patch: true, - update: data - }); - }, - - save: function(options) { - var method, - patch = options && options.patch ? true : false, - config = this.getConfig(options); - - /** - * Simplified version of code from the default Backbone save function - * http://backbonejs.org/docs/backbone.html#section-87 - */ - method = this.isNew() ? 'create' : (patch ? 'patch' : 'update'); - - this.sync(method, this, config); - }, - - saveError: function(jqXHR) { - this.trigger('error', jqXHR); - }, - - saveSuccess: function(data) { - this.set({id: data.id}); - this.trigger('sync', this); - } - }); - } -); diff --git a/cms/static/js/programs/program_admin_app.js b/cms/static/js/programs/program_admin_app.js deleted file mode 100644 index b4c023717e..0000000000 --- a/cms/static/js/programs/program_admin_app.js +++ /dev/null @@ -1,10 +0,0 @@ -(function() { - 'use strict'; - require([ - 'js/programs/views/program_admin_app_view' - ], - function(ProgramAdminAppView) { - return new ProgramAdminAppView(); - } - ); -})(); diff --git a/cms/static/js/programs/router.js b/cms/static/js/programs/router.js deleted file mode 100644 index 1a8932a30c..0000000000 --- a/cms/static/js/programs/router.js +++ /dev/null @@ -1,65 +0,0 @@ -define([ - 'backbone', - 'js/programs/views/program_creator_view', - 'js/programs/views/program_details_view', - 'js/programs/models/program_model' -], - function(Backbone, ProgramCreatorView, ProgramDetailsView, ProgramModel) { - 'use strict'; - - return Backbone.Router.extend({ - root: '/program/', - - routes: { - 'new': 'programCreator', - ':id': 'programDetails' - }, - - initialize: function(options) { - this.homeUrl = options.homeUrl; - }, - - goHome: function() { - window.location.href = this.homeUrl; - }, - - loadProgramDetails: function() { - this.programDetailsView = new ProgramDetailsView({ - model: this.programModel - }); - }, - - programCreator: function() { - if (this.programCreatorView) { - this.programCreatorView.destroy(); - } - - this.programCreatorView = new ProgramCreatorView({ - router: this - }); - }, - - programDetails: function(id) { - this.programModel = new ProgramModel({ - id: id - }); - - this.programModel.on('sync', this.loadProgramDetails, this); - this.programModel.fetch(); - }, - - /** - * Starts the router. - */ - start: function() { - if (!Backbone.history.started) { - Backbone.history.start({ - pushState: true, - root: this.root - }); - } - return this; - } - }); - } -); diff --git a/cms/static/js/programs/shims/gettext.js b/cms/static/js/programs/shims/gettext.js deleted file mode 100644 index 69384c3158..0000000000 --- a/cms/static/js/programs/shims/gettext.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * the Programs application loads gettext identity library via django, thus - * components reference gettext globally so a shim is added here to reflect - * the text so tests can be run if modules reference gettext - */ -(function() { - 'use strict'; - - if (!window.gettext) { - window.gettext = function(text) { - return text; - }; - } - - if (!window.interpolate) { - window.interpolate = function(text) { - return text; - }; - } - - return window; -})(); diff --git a/cms/static/js/programs/utils/api_config.js b/cms/static/js/programs/utils/api_config.js deleted file mode 100644 index e1e02f5c7e..0000000000 --- a/cms/static/js/programs/utils/api_config.js +++ /dev/null @@ -1,20 +0,0 @@ -define([ - 'js/programs/models/api_config_model' -], - function(ApiConfigModel) { - 'use strict'; - - /** - * This js module implements the Singleton pattern for an instance - * of the ApiConfigModel Backbone model. It returns the same shared - * instance of that model anywhere it is required. - */ - var instance; - - if (instance === undefined) { - instance = new ApiConfigModel(); - } - - return instance; - } -); diff --git a/cms/static/js/programs/utils/auth_utils.js b/cms/static/js/programs/utils/auth_utils.js deleted file mode 100644 index 3f2d7ee823..0000000000 --- a/cms/static/js/programs/utils/auth_utils.js +++ /dev/null @@ -1,87 +0,0 @@ -define([ - 'jquery', - 'underscore', - 'js/programs/utils/api_config' -], - function($, _, apiConfig) { - 'use strict'; - - var auth = { - autoSync: { - /** - * Override Backbone.sync to seamlessly attempt (re-)authentication when necessary. - * - * If a 401 error response is encountered while making a request to the Programs, - * API, this wrapper will attempt to request an id token from a custom endpoint - * via AJAX. Then the original request will be retried once more. - * - * Any other response than 401 on the original API request, or any error occurring - * on the retried API request (including 401), will be handled by the base sync - * implementation. - * - */ - sync: function(method, model, options) { - var oldError = options.error; - - this._setHeaders(options); - - options.notifyOnError = false; // suppress Studio error pop-up that will happen if we get a 401 - - options.error = function(xhr, textStatus, errorThrown) { - if (xhr && xhr.status === 401) { - // attempt auth and retry - this._updateToken(function() { - // restore the original error handler - options.error = oldError; - options.notifyOnError = true; // if it fails again, let Studio notify. - delete options.xhr; // remove the failed (401) xhr from the last try. - - // update authorization header - this._setHeaders(options); - - Backbone.sync.call(this, method, model, options); - }.bind(this)); - } else if (oldError) { - // fall back to the original error handler - oldError.call(this, xhr, textStatus, errorThrown); - } - }.bind(this); - return Backbone.sync.call(this, method, model, options); - }, - - /** - * Fix up headers on an imminent AJAX sync, ensuring that the JWT token is enclosed - * and that credentials are included when the request is being made cross-domain. - */ - _setHeaders: function(ajaxOptions) { - ajaxOptions.headers = _.extend(ajaxOptions.headers || {}, { - Authorization: 'JWT ' + apiConfig.get('idToken') - }); - ajaxOptions.xhrFields = _.extend(ajaxOptions.xhrFields || {}, { - withCredentials: true - }); - }, - - /** - * Fetch a new id token from the configured endpoint, update the api config, - * and invoke the specified callback. - */ - _updateToken: function(success) { - $.ajax({ - url: apiConfig.get('authUrl'), - xhrFields: { - // See: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials - withCredentials: true - }, - crossDomain: true - }).done(function(data) { - // save the newly-retrieved id token - apiConfig.set('idToken', data.id_token); - }).done(success); - } - } - }; - - return auth; - } -); diff --git a/cms/static/js/programs/utils/constants.js b/cms/static/js/programs/utils/constants.js deleted file mode 100644 index e60dea9a0b..0000000000 --- a/cms/static/js/programs/utils/constants.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Reusable constants - */ -define([], function() { - 'use strict'; - - return { - keyCodes: { - tab: 9, - enter: 13, - esc: 27, - up: 38, - down: 40 - } - }; -}); diff --git a/cms/static/js/programs/utils/validation_config.js b/cms/static/js/programs/utils/validation_config.js deleted file mode 100644 index 998d890090..0000000000 --- a/cms/static/js/programs/utils/validation_config.js +++ /dev/null @@ -1,70 +0,0 @@ -define([ - 'backbone', - 'backbone.validation', - 'underscore', - 'gettext' -], - function(Backbone, BackboneValidation, _, gettext) { - 'use strict'; - - var errorClass = 'has-error', - messageEl = '.field-message', - messageContent = '.field-message-content'; - - // These are the same messages provided by Backbone.Validation, - // marked for translation. - // See: http://thedersen.com/projects/backbone-validation/#overriding-the-default-error-messages. - _.extend(Backbone.Validation.messages, { - required: gettext('{0} is required'), - acceptance: gettext('{0} must be accepted'), - min: gettext('{0} must be greater than or equal to {1}'), - max: gettext('{0} must be less than or equal to {1}'), - range: gettext('{0} must be between {1} and {2}'), - length: gettext('{0} must be {1} characters'), - minLength: gettext('{0} must be at least {1} characters'), - maxLength: gettext('{0} must be at most {1} characters'), - rangeLength: gettext('{0} must be between {1} and {2} characters'), - oneOf: gettext('{0} must be one of: {1}'), - equalTo: gettext('{0} must be the same as {1}'), - digits: gettext('{0} must only contain digits'), - number: gettext('{0} must be a number'), - email: gettext('{0} must be a valid email'), - url: gettext('{0} must be a valid url'), - inlinePattern: gettext('{0} is invalid') - }); - - _.extend(Backbone.Validation.callbacks, { - // Gets called when a previously invalid field in the - // view becomes valid. Removes any error message. - valid: function(view, attr, selector) { - var $input = view.$('[' + selector + '~="' + attr + '"]'), - $message = $input.siblings(messageEl); - - $input.removeClass(errorClass) - .removeAttr('data-error'); - - $message.removeClass(errorClass) - .find(messageContent) - .text(''); - }, - - // Gets called when a field in the view becomes invalid. - // Adds a error message. - invalid: function(view, attr, error, selector) { - var $input = view.$('[' + selector + '~="' + attr + '"]'), - $message = $input.siblings(messageEl); - - $input.addClass(errorClass) - .attr('data-error', error); - - $message.addClass(errorClass) - .find(messageContent) - .text($input.data('error')); - } - }); - - Backbone.Validation.configure({ - labelFormatter: 'label' - }); - } -); diff --git a/cms/static/js/programs/views/confirm_modal_view.js b/cms/static/js/programs/views/confirm_modal_view.js deleted file mode 100644 index 459103de86..0000000000 --- a/cms/static/js/programs/views/confirm_modal_view.js +++ /dev/null @@ -1,59 +0,0 @@ -define([ - 'backbone', - 'jquery', - 'underscore', - 'js/programs/utils/constants', - 'text!templates/programs/confirm_modal.underscore', - 'edx-ui-toolkit/js/utils/html-utils', - 'gettext' -], - function(Backbone, $, _, constants, ModalTpl, HtmlUtils) { - 'use strict'; - - return Backbone.View.extend({ - events: { - 'click .js-cancel': 'destroy', - 'click .js-confirm': 'confirm', - 'keydown': 'handleKeydown' - }, - - tpl: HtmlUtils.template(ModalTpl), - - initialize: function(options) { - this.$parentEl = $(options.parentEl); - this.callback = options.callback; - this.content = options.content; - this.render(); - }, - - render: function() { - HtmlUtils.setHtml(this.$el, this.tpl(this.content)); - HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el)); - this.postRender(); - }, - - postRender: function() { - this.$el.find('.js-focus-first').focus(); - }, - - confirm: function() { - this.callback(); - this.destroy(); - }, - - destroy: function() { - this.undelegateEvents(); - this.remove(); - this.$parentEl.html(''); - }, - - handleKeydown: function(event) { - var keyCode = event.keyCode; - - if (keyCode === constants.keyCodes.esc) { - this.destroy(); - } - } - }); - } -); diff --git a/cms/static/js/programs/views/course_details_view.js b/cms/static/js/programs/views/course_details_view.js deleted file mode 100644 index 316ccb5f98..0000000000 --- a/cms/static/js/programs/views/course_details_view.js +++ /dev/null @@ -1,204 +0,0 @@ -define([ - 'backbone', - 'backbone.validation', - 'jquery', - 'underscore', - 'js/programs/models/course_model', - 'js/programs/models/course_run_model', - 'js/programs/models/program_model', - 'js/programs/views/course_run_view', - 'text!templates/programs/course_details.underscore', - 'edx-ui-toolkit/js/utils/html-utils', - 'gettext', - 'js/programs/utils/validation_config' -], - function(Backbone, BackboneValidation, $, _, CourseModel, CourseRunModel, - ProgramModel, CourseRunView, ListTpl, HtmlUtils) { - 'use strict'; - - return Backbone.View.extend({ - parentEl: '.js-course-list', - - className: 'course-details', - - events: { - 'click .js-remove-course': 'destroy', - 'click .js-select-course': 'setCourse', - 'click .js-add-course-run': 'addCourseRun' - }, - - tpl: HtmlUtils.template(ListTpl), - - initialize: function(options) { - this.model = new CourseModel(); - Backbone.Validation.bind(this); - this.$parentEl = $(this.parentEl); - - // For managing subViews - this.courseRunViews = []; - this.courseRuns = options.courseRuns; - this.programModel = options.programModel; - - if (options.courseData) { - this.model.set(options.courseData); - } else { - this.model.set({run_modes: []}); - } - - // Need a unique value for field ids so using model cid - this.model.set({cid: this.model.cid}); - this.model.on('change:run_modes', this.updateRuns, this); - this.render(); - }, - - render: function() { - HtmlUtils.setHtml(this.$el, this.tpl(this.formatData())); - this.$parentEl.append(this.$el); - this.postRender(); - }, - - postRender: function() { - var runs = this.model.get('run_modes'); - if (runs && runs.length > 0) { - this.addCourseRuns(); - } - }, - - addCourseRun: function(event) { - var $runsContainer = this.$el.find('.js-course-runs'), - runModel = new CourseRunModel(), - runView; - - event.preventDefault(); - - runModel.set({course_key: undefined}); - - runView = new CourseRunView({ - model: runModel, - courseModel: this.model, - courseRuns: this.courseRuns, - programStatus: this.programModel.get('status'), - $parentEl: $runsContainer - }); - - this.courseRunViews.push(runView); - }, - - addCourseRuns: function() { - // Create run views - var runs = this.model.get('run_modes'), - $runsContainer = this.$el.find('.js-course-runs'); - - _.each(runs, function(run) { - var runModel = new CourseRunModel(), - runView; - - runModel.set(run); - - runView = new CourseRunView({ - model: runModel, - courseModel: this.model, - courseRuns: this.courseRuns, - programStatus: this.programModel.get('status'), - $parentEl: $runsContainer - }); - - this.courseRunViews.push(runView); - - return runView; - }.bind(this)); - }, - - addCourseToProgram: function() { - var courseCodes = this.programModel.get('course_codes'), - courseData = this.model.toJSON(); - - if (this.programModel.isValid(true)) { - // We don't want to save the cid so omit it - courseCodes.push(_.omit(courseData, 'cid')); - this.programModel.patch({course_codes: courseCodes}); - } - }, - // Delete this view - destroy: function() { - Backbone.Validation.unbind(this); - this.destroyChildren(); - this.undelegateEvents(); - this.removeCourseFromProgram(); - this.remove(); - }, - - destroyChildren: function() { - var runs = this.courseRunViews; - - _.each(runs, function(run) { - run.removeRun(); - }); - }, - - // Format data to be passed to the template - formatData: function() { - var data = $.extend({}, - {courseRuns: this.courseRuns.models}, - _.omit(this.programModel.toJSON(), 'run_modes'), - this.model.toJSON() - ); - - return data; - }, - - removeCourseFromProgram: function() { - var courseCodes = this.programModel.get('course_codes'), - key = this.model.get('key'), - name = this.model.get('display_name'), - update = []; - - update = _.reject(courseCodes, function(course) { - return course.key === key && course.display_name === name; - }); - - this.programModel.patch({course_codes: update}); - }, - - setCourse: function(event) { - var $form = this.$('.js-course-form'), - title = $form.find('.display-name').val(), - key = $form.find('.course-key').val(); - - event.preventDefault(); - - this.model.set({ - display_name: title, - key: key, - organization: this.programModel.get('organizations')[0] - }); - - if (this.model.isValid(true)) { - this.addCourseToProgram(); - this.updateDOM(); - this.addCourseRuns(); - } - }, - - updateDOM: function() { - HtmlUtils.setHtml(this.$el, this.tpl(this.formatData())); - }, - - updateRuns: function() { - var courseCodes = this.programModel.get('course_codes'), - key = this.model.get('key'), - name = this.model.get('display_name'), - index; - - if (this.programModel.isValid(true)) { - index = _.findIndex(courseCodes, function(course) { - return course.key === key && course.display_name === name; - }); - courseCodes[index] = this.model.toJSON(); - - this.programModel.patch({course_codes: courseCodes}); - } - } - }); - } -); diff --git a/cms/static/js/programs/views/course_run_view.js b/cms/static/js/programs/views/course_run_view.js deleted file mode 100644 index 1b01805db5..0000000000 --- a/cms/static/js/programs/views/course_run_view.js +++ /dev/null @@ -1,113 +0,0 @@ -define([ - 'backbone', - 'jquery', - 'underscore', - 'text!templates/programs/course_run.underscore', - 'edx-ui-toolkit/js/utils/html-utils' -], - function(Backbone, $, _, CourseRunTpl, HtmlUtils) { - 'use strict'; - - return Backbone.View.extend({ - events: { - 'change .js-course-run-select': 'selectRun', - 'click .js-remove-run': 'removeRun' - }, - - tpl: HtmlUtils.template(CourseRunTpl), - - initialize: function(options) { - /** - * Need the run model for the template, and the courseModel - * to keep parent view up to date with run changes - */ - this.courseModel = options.courseModel; - this.courseRuns = options.courseRuns; - this.programStatus = options.programStatus; - - this.model.on('change', this.render, this); - this.courseRuns.on('update', this.updateDropdown, this); - - this.$parentEl = options.$parentEl; - this.render(); - }, - - render: function() { - var data = this.model.attributes; - - data.programStatus = this.programStatus; - - if (!!this.courseRuns) { - data.courseRuns = this.courseRuns.toJSON(); - } - - HtmlUtils.setHtml(this.$el, this.tpl(data)); - this.$parentEl.append(this.$el); - }, - - // Delete this view - destroy: function() { - this.undelegateEvents(); - this.remove(); - }, - - // Data returned from courseList API is not the correct format - formatData: function(data) { - return { - course_key: data.id, - mode_slug: 'verified', - start_date: data.start, - sku: '' - }; - }, - - removeRun: function() { - // Update run_modes array on programModel - var startDate = this.model.get('start_date'), - courseKey = this.model.get('course_key'), - /** - * NB: cloning the array so the model will fire a change event when - * the updated version is saved back to the model - */ - runs = _.clone(this.courseModel.get('run_modes')), - updatedRuns = []; - - updatedRuns = _.reject(runs, function(obj) { - return obj.start_date === startDate && - obj.course_key === courseKey; - }); - - this.courseModel.set({ - run_modes: updatedRuns - }); - - this.courseRuns.addRun(courseKey); - - this.destroy(); - }, - - selectRun: function(event) { - var id = $(event.currentTarget).val(), - runObj = _.findWhere(this.courseRuns.allRuns, {id: id}), - /** - * NB: cloning the array so the model will fire a change event when - * the updated version is saved back to the model - */ - runs = _.clone(this.courseModel.get('run_modes')), - data = this.formatData(runObj); - - this.model.set(data); - runs.push(data); - this.courseModel.set({run_modes: runs}); - this.courseRuns.removeRun(id); - }, - - // If a run has not been selected update the dropdown options - updateDropdown: function() { - if (!this.model.get('course_key')) { - this.render(); - } - } - }); - } -); diff --git a/cms/static/js/programs/views/program_admin_app_view.js b/cms/static/js/programs/views/program_admin_app_view.js deleted file mode 100644 index 0a5542059f..0000000000 --- a/cms/static/js/programs/views/program_admin_app_view.js +++ /dev/null @@ -1,68 +0,0 @@ -(function() { - 'use strict'; - - define([ - 'backbone', - 'js/programs/router', - 'js/programs/utils/api_config' - ], - function(Backbone, ProgramRouter, apiConfig) { - return Backbone.View.extend({ - el: '.js-program-admin', - - events: { - 'click .js-app-click': 'navigate' - }, - - initialize: function() { - apiConfig.set({ - lmsBaseUrl: this.$el.data('lms-base-url'), - programsApiUrl: this.$el.data('programs-api-url'), - authUrl: this.$el.data('auth-url'), - username: this.$el.data('username') - }); - - this.app = new ProgramRouter({ - homeUrl: this.$el.data('home-url') - }); - this.app.start(); - }, - - /** - * Navigate to a new page within the app. - * - * Attempts to open the link in a new tab/window behave as the user expects, however the app - * and data will be reloaded in the new tab/window. - * - * @param {Event} event - Event being handled. - * @returns {boolean} - Indicates if event handling succeeded (always true). - */ - navigate: function(event) { - var url = $(event.target).attr('href').replace(this.app.root, ''); - - /** - * Handle the cases where the user wants to open the link in a new tab/window. - * event.which === 2 checks for the middle mouse button (https://api.jquery.com/event.which/) - */ - if (event.ctrlKey || event.shiftKey || event.metaKey || event.which === 2) { - return true; - } - - // We'll take it from here... - event.preventDefault(); - - // Process the navigation in the app/router. - if (url === Backbone.history.getFragment() && url === '') { - /** - * Note: We must call the index directly since Backbone - * does not support routing to the same route. - */ - this.app.index(); - } else { - this.app.navigate(url, {trigger: true}); - } - } - }); - } - ); -})(); diff --git a/cms/static/js/programs/views/program_creator_view.js b/cms/static/js/programs/views/program_creator_view.js deleted file mode 100644 index 32f9e59820..0000000000 --- a/cms/static/js/programs/views/program_creator_view.js +++ /dev/null @@ -1,112 +0,0 @@ -define([ - 'backbone', - 'backbone.validation', - 'jquery', - 'underscore', - 'js/programs/models/organizations_model', - 'js/programs/models/program_model', - 'text!templates/programs/program_creator_form.underscore', - 'edx-ui-toolkit/js/utils/html-utils', - 'gettext', - 'js/programs/utils/validation_config' -], - function(Backbone, BackboneValidation, $, _, OrganizationsModel, ProgramModel, ListTpl, HtmlUtils) { - 'use strict'; - - return Backbone.View.extend({ - parentEl: '.js-program-admin', - - events: { - 'click .js-create-program': 'createProgram', - 'click .js-abort-view': 'abort' - }, - - tpl: HtmlUtils.template(ListTpl), - - initialize: function(options) { - this.$parentEl = $(this.parentEl); - - this.model = new ProgramModel(); - this.model.on('sync', this.saveSuccess, this); - this.model.on('error', this.saveError, this); - - // Hook up validation. - // See: http://thedersen.com/projects/backbone-validation/#validation-binding. - Backbone.Validation.bind(this); - - this.organizations = new OrganizationsModel(); - this.organizations.on('sync', this.render, this); - this.organizations.fetch(); - - this.router = options.router; - }, - - render: function() { - HtmlUtils.setHtml( - this.$el, - this.tpl({ - orgs: this.organizations.get('results') - }) - ); - - HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el)); - }, - - abort: function(event) { - event.preventDefault(); - this.router.goHome(); - }, - - createProgram: function(event) { - var data = this.getData(); - - event.preventDefault(); - this.model.set(data); - - // Check if the model is valid before saving. Invalid attributes are looked - // up by name. The corresponding elements receieve an `invalid` class and a - // `data-error` attribute. Both are removed when formerly invalid attributes - // become valid. - // See: http://thedersen.com/projects/backbone-validation/#isvalid. - if (this.model.isValid(true)) { - this.model.save(); - } - }, - - destroy: function() { - // Unhook validation. - // See: http://thedersen.com/projects/backbone-validation/#unbinding. - Backbone.Validation.unbind(this); - - this.undelegateEvents(); - this.remove(); - }, - - getData: function() { - return { - name: this.$el.find('.program-name').val(), - subtitle: this.$el.find('.program-subtitle').val(), - category: this.$el.find('.program-type').val(), - marketing_slug: this.$el.find('.program-marketing-slug').val(), - organizations: [{ - key: this.$el.find('.program-org').val() - }] - }; - }, - - goToView: function(uri) { - Backbone.history.navigate(uri, {trigger: true}); - this.destroy(); - }, - - // TODO: add user messaging to show errors - saveError: function(jqXHR) { - console.log('saveError: ', jqXHR); - }, - - saveSuccess: function() { - this.goToView(String(this.model.get('id'))); - } - }); - } -); diff --git a/cms/static/js/programs/views/program_details_view.js b/cms/static/js/programs/views/program_details_view.js deleted file mode 100644 index 6c7534ebf2..0000000000 --- a/cms/static/js/programs/views/program_details_view.js +++ /dev/null @@ -1,206 +0,0 @@ -define([ - 'backbone', - 'backbone.validation', - 'jquery', - 'underscore', - 'js/programs/collections/course_runs_collection', - 'js/programs/models/program_model', - 'js/programs/views/confirm_modal_view', - 'js/programs/views/course_details_view', - 'text!templates/programs/program_details.underscore', - 'edx-ui-toolkit/js/utils/html-utils', - 'gettext', - 'js/programs/utils/validation_config' -], - function(Backbone, BackboneValidation, $, _, CourseRunsCollection, - ProgramModel, ModalView, CourseView, ListTpl, - HtmlUtils) { - 'use strict'; - - return Backbone.View.extend({ - el: '.js-program-admin', - - events: { - 'blur .js-inline-edit input': 'checkEdit', - 'click .js-add-course': 'addCourse', - 'click .js-enable-edit': 'editField', - 'click .js-publish-program': 'confirmPublish' - }, - - tpl: HtmlUtils.template(ListTpl), - - initialize: function() { - Backbone.Validation.bind(this); - - this.courseRuns = new CourseRunsCollection([], { - organization: this.model.get('organizations')[0] - }); - this.courseRuns.fetch(); - this.courseRuns.on('sync', this.setAvailableCourseRuns, this); - this.render(); - }, - - render: function() { - HtmlUtils.setHtml(this.$el, this.tpl(this.model.toJSON())); - this.postRender(); - }, - - postRender: function() { - var courses = this.model.get('course_codes'); - - _.each(courses, function(course) { - var title = course.key + 'Course'; - - this[title] = new CourseView({ - courseRuns: this.courseRuns, - programModel: this.model, - courseData: course - }); - }.bind(this)); - - // Stop listening to the model sync set when publishing - this.model.off('sync'); - }, - - addCourse: function() { - return new CourseView({ - courseRuns: this.courseRuns, - programModel: this.model - }); - }, - - checkEdit: function(event) { - var $input = $(event.target), - $span = $input.prevAll('.js-model-value'), - $btn = $input.next('.js-enable-edit'), - value = $input.val(), - key = $input.data('field'), - data = {}; - - data[key] = value; - - $input.addClass('is-hidden'); - $btn.removeClass('is-hidden'); - $span.removeClass('is-hidden'); - - if (this.model.get(key) !== value) { - this.model.set(data); - - if (this.model.isValid(true)) { - this.model.patch(data); - $span.text(value); - } - } - }, - - /** - * Loads modal that user clicks a confirmation button - * in to publish the course (or they can cancel out of it) - */ - confirmPublish: function(event) { - event.preventDefault(); - - /** - * Update validation to make marketing slug required - * Note that because this validation is not required for - * the program creation form and is only happening here - * it makes sense to have the validation at the view level - */ - if (this.model.isValid(true) && this.validateMarketingSlug()) { - this.modalView = new ModalView({ - model: this.model, - callback: _.bind(this.publishProgram, this), - content: this.getModalContent(), - parentEl: '.js-publish-modal', - parentView: this - }); - } - }, - - editField: function(event) { - /** - * Making the assumption that users can only see - * programs that they have permission to edit - */ - var $btn = $(event.currentTarget), - $el = $btn.prev('input'); - - event.preventDefault(); - - $el.prevAll('.js-model-value').addClass('is-hidden'); - $el.removeClass('is-hidden') - .addClass('edit') - .focus(); - $btn.addClass('is-hidden'); - }, - - getModalContent: function() { - return { - name: gettext('confirm'), - title: gettext('Publish this program?'), - body: gettext( - 'After you publish this program, you cannot add or remove course codes or remove course runs.' - ), - cta: { - cancel: gettext('Cancel'), - confirm: gettext('Publish') - } - }; - }, - - publishProgram: function() { - var data = { - status: 'active' - }; - - this.model.set(data, {silent: true}); - this.model.on('sync', this.render, this); - this.model.patch(data); - }, - - setAvailableCourseRuns: function() { - var allRuns = this.courseRuns.toJSON(), - courses = this.model.get('course_codes'), - selectedRuns, - availableRuns = allRuns; - - if (courses.length) { - selectedRuns = _.pluck(courses, 'run_modes'); - selectedRuns = _.flatten(selectedRuns); - } - - availableRuns = _.reject(allRuns, function(run) { - var selectedCourseRun = _.findWhere(selectedRuns, { - course_key: run.id, - start_date: run.start - }); - - return !_.isUndefined(selectedCourseRun); - }); - - this.courseRuns.set(availableRuns); - }, - - validateMarketingSlug: function() { - var isValid = false, - $input = {}, - $message = {}; - - if (this.model.get('marketing_slug').length > 0) { - isValid = true; - } else { - $input = this.$el.find('#program-marketing-slug'); - $message = $input.siblings('.field-message'); - - // Update DOM - $input.addClass('has-error'); - $message.addClass('has-error'); - $message.find('.field-message-content') - .text(gettext('Marketing Slug is required.')); - } - - return isValid; - } - }); - } -); diff --git a/cms/static/js/spec/models/auto_auth_model_spec.js b/cms/static/js/spec/models/auto_auth_model_spec.js deleted file mode 100644 index 4d31193e39..0000000000 --- a/cms/static/js/spec/models/auto_auth_model_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -define([ - 'underscore', - 'backbone', - 'jquery', - 'js/programs/utils/api_config', - 'js/programs/models/auto_auth_model' -], - function(_, Backbone, $, apiConfig, AutoAuthModel) { - 'use strict'; - - describe('AutoAuthModel', function() { - var model, - testErrorCallback, - fakeAjaxDeferred, - spyOnBackboneSync, - callSync, - checkAuthAttempted, - dummyModel = {'dummy': 'model'}, - authUrl = apiConfig.get('authUrl'); - - beforeEach(function() { - // instance under test - model = new AutoAuthModel(); - - // stand-in for the error callback a caller might pass with options to Backbone.Model.sync - testErrorCallback = jasmine.createSpy(); - - fakeAjaxDeferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(fakeAjaxDeferred); - return fakeAjaxDeferred; - }); - - spyOnBackboneSync = function(status) { - // set up Backbone.sync to invoke its error callback with the desired HTTP status - spyOn(Backbone, 'sync').and.callFake(function(method, model, options) { - var fakeXhr = options.xhr = {status: status}; - options.error(fakeXhr, 0, ''); - }); - }; - - callSync = function(options) { - var params, - syncOptions = _.extend({error: testErrorCallback}, options || {}); - - model.sync('GET', dummyModel, syncOptions); - - // make sure Backbone.sync was called with custom error handling - expect(Backbone.sync.calls.count()).toEqual(1); - params = _.object(['method', 'model', 'options'], Backbone.sync.calls.mostRecent().args); - expect(params.method).toEqual('GET'); - expect(params.model).toEqual(dummyModel); - expect(params.options.error).not.toEqual(testErrorCallback); - return params; - }; - - checkAuthAttempted = function(isExpected) { - if (isExpected) { - expect($.ajax).toHaveBeenCalled(); - expect($.ajax.calls.mostRecent().args[0].url).toEqual(authUrl); - } else { - expect($.ajax).not.toHaveBeenCalled(); - } - }; - - it('should exist', function() { - expect(model).toBeDefined(); - }); - - it('should intercept 401 errors and attempt auth', function() { - var callParams; - - spyOnBackboneSync(401); - - callSync(); - - // make sure the auth attempt was initiated - checkAuthAttempted(true); - - // fire the success handler for the fake ajax call, with id token response data - fakeAjaxDeferred.resolve({id_token: 'test-id-token'}); - - // make sure the original request was retried with token, and without custom error handling - expect(Backbone.sync.calls.count()).toEqual(2); - callParams = _.object(['method', 'model', 'options'], Backbone.sync.calls.mostRecent().args); - expect(callParams.method).toEqual('GET'); - expect(callParams.model).toEqual(dummyModel); - expect(callParams.options.error).toEqual(testErrorCallback); - expect(callParams.options.headers.Authorization).toEqual('JWT test-id-token'); - }); - - it('should not intercept non-401 errors', function() { - spyOnBackboneSync(403); - - // invoke AutoAuthModel.sync - callSync(); - - // make sure NO auth attempt was initiated - checkAuthAttempted(false); - - // make sure the original request was not retried - expect(Backbone.sync.calls.count()).toEqual(1); - - // make sure the default error handling was invoked - expect(testErrorCallback).toHaveBeenCalled(); - }); - }); - } -); diff --git a/cms/static/js/spec/views/programs/program_creator_spec.js b/cms/static/js/spec/views/programs/program_creator_spec.js deleted file mode 100644 index 98749a1f80..0000000000 --- a/cms/static/js/spec/views/programs/program_creator_spec.js +++ /dev/null @@ -1,252 +0,0 @@ -define([ - 'backbone', - 'jquery', - 'js/programs/views/program_creator_view' -], - function(Backbone, $, ProgramCreatorView) { - 'use strict'; - - describe('ProgramCreatorView', function() { - var view = {}, - Router = Backbone.Router.extend({ - initialize: function(options) { - this.homeUrl = options.homeUrl; - }, - goHome: function() { - window.location.href = this.homeUrl; - } - }), - organizations = { - count: 1, - previous: null, - 'num_pages': 1, - results: [{ - 'display_name': 'test-org-display_name', - 'key': 'test-org-key' - }], - next: null - }, - sampleInput, - completeForm = function(data) { - view.$el.find('#program-name').val(data.name); - view.$el.find('#program-subtitle').val(data.subtitle); - view.$el.find('#program-org').val(data.organizations); - - if (data.category) { - view.$el.find('#program-type').val(data.category); - } - - if (data.marketing_slug) { - view.$el.find('#program-marketing-slug').val(data.marketing_slug); - } - }, - verifyValidation = function(data, invalidAttr) { - var errorClass = 'has-error', - $invalidElement = view.$el.find('[name="' + invalidAttr + '"]'), - $errorMsg = $invalidElement.siblings('.field-message'), - inputErrorMsg = ''; - - completeForm(data); - - view.$el.find('.js-create-program').click(); - inputErrorMsg = $invalidElement.data('error'); - - expect(view.model.save).not.toHaveBeenCalled(); - expect($invalidElement).toHaveClass(errorClass); - expect($errorMsg).toHaveClass(errorClass); - expect(inputErrorMsg).toBeDefined(); - expect($errorMsg.find('.field-message-content').html()).toEqual(inputErrorMsg); - }; - - var validateFormSubmitted = function(view, programId) { - expect($.ajax).toHaveBeenCalled(); - expect(view.saveSuccess).toHaveBeenCalled(); - expect(view.goToView).toHaveBeenCalledWith(String(programId)); - expect(view.saveError).not.toHaveBeenCalled(); - }; - - beforeEach(function() { - // Set the DOM - setFixtures('
'); - - jasmine.clock().install(); - - spyOn(ProgramCreatorView.prototype, 'saveSuccess').and.callThrough(); - spyOn(ProgramCreatorView.prototype, 'goToView').and.callThrough(); - spyOn(ProgramCreatorView.prototype, 'saveError').and.callThrough(); - spyOn(Router.prototype, 'goHome'); - - sampleInput = { - category: 'XSeries', - organizations: 'test-org-key', - name: 'Test Course Name', - subtitle: 'Test Course Subtitle', - marketing_slug: 'test-management' - }; - - view = new ProgramCreatorView({ - router: new Router({ - homeUrl: '/author/home' - }) - }); - - view.organizations.set(organizations); - view.render(); - }); - - afterEach(function() { - view.destroy(); - - jasmine.clock().uninstall(); - }); - - it('should exist', function() { - expect(view).toBeDefined(); - }); - - it('should get the form data', function() { - var formData = {}; - - completeForm(sampleInput); - formData = view.getData(); - - expect(formData.name).toEqual(sampleInput.name); - expect(formData.subtitle).toEqual(sampleInput.subtitle); - expect(formData.organizations[0].key).toEqual(sampleInput.organizations); - }); - - it('should submit the form when the user clicks submit', function() { - var programId = 123; - - completeForm(sampleInput); - - spyOn($, 'ajax').and.callFake(function(event) { - event.success({id: programId}); - }); - - view.$el.find('.js-create-program').click(); - - validateFormSubmitted(view, programId); - }); - - it('should submit the form correctly when creating micromasters program ', function() { - var programId = 221; - sampleInput.category = 'MicroMasters'; - - completeForm(sampleInput); - - spyOn($, 'ajax').and.callFake(function(event) { - event.success({id: programId}); - }); - - view.$el.find('.js-create-program').click(); - - validateFormSubmitted(view, programId); - }); - - it('should run the saveError when model save failures occur', function() { - spyOn($, 'ajax').and.callFake(function(event) { - event.error(); - }); - - // Fill out the form with valid data so that form model validation doesn't - // prevent the model from being saved. - completeForm(sampleInput); - view.$el.find('.js-create-program').click(); - - expect($.ajax).toHaveBeenCalled(); - expect(view.saveSuccess).not.toHaveBeenCalled(); - expect(view.saveError).toHaveBeenCalled(); - }); - - it('should set the model when valid form data is submitted', function() { - completeForm(sampleInput); - - spyOn($, 'ajax').and.callFake(function(event) { - event.success({id: 10001110101}); - }); - - view.$el.find('.js-create-program').click(); - - expect(view.model.get('name')).toEqual(sampleInput.name); - expect(view.model.get('subtitle')).toEqual(sampleInput.subtitle); - expect(view.model.get('organizations')[0].key).toEqual(sampleInput.organizations); - expect(view.model.get('marketing_slug')).toEqual(sampleInput.marketing_slug); - }); - - it('should not set the model when bad program type selected', function() { - var invalidInput = $.extend({}, sampleInput); - spyOn(view.model, 'save'); - - // No name provided. - invalidInput.category = ''; - verifyValidation(invalidInput, 'category'); - - // bad program type name - invalidInput.name = 'badprogramtype'; - verifyValidation(invalidInput, 'category'); - }); - - it('should not set the model when an invalid program name is submitted', function() { - var invalidInput = $.extend({}, sampleInput); - - spyOn(view.model, 'save'); - - // No name provided. - invalidInput.name = ''; - verifyValidation(invalidInput, 'name'); - - // Name is too long. - invalidInput.name = 'x'.repeat(256); - verifyValidation(invalidInput, 'name'); - }); - - it('should not set the model when an invalid program subtitle is submitted', function() { - var invalidInput = $.extend({}, sampleInput); - - spyOn(view.model, 'save'); - - // Subtitle is too long. - invalidInput.subtitle = 'x'.repeat(300); - verifyValidation(invalidInput, 'subtitle'); - }); - - it('should not set the model when an invalid category is submitted', function() { - var invalidInput = $.extend({}, sampleInput); - - spyOn(view.model, 'save'); - - // Category other than 'xseries' selected. - invalidInput.category = 'yseries'; - verifyValidation(invalidInput, 'category'); - }); - - it('should not set the model when an invalid organization key is submitted', function() { - var invalidInput = $.extend({}, sampleInput); - - spyOn(view.model, 'save'); - - // No organization selected. - invalidInput.organizations = 'false'; - verifyValidation(invalidInput, 'organizations'); - }); - - it('should not set the model when an invalid marketing slug is submitted', function() { - var invalidInput = $.extend({}, sampleInput); - - spyOn(view.model, 'save'); - - // Marketing slug is too long. - invalidInput.marketing_slug = 'x'.repeat(256); - verifyValidation(invalidInput, 'marketing_slug'); - }); - - it('should abort the view when the cancel button is clicked', function() { - completeForm(sampleInput); - expect(view.$parentEl.html().length).toBeGreaterThan(0); - view.$el.find('.js-abort-view').click(); - expect(view.router.goHome).toHaveBeenCalled(); - }); - }); - } -); diff --git a/cms/static/js/spec/views/programs/program_details_spec.js b/cms/static/js/spec/views/programs/program_details_spec.js deleted file mode 100644 index e3b470a3ac..0000000000 --- a/cms/static/js/spec/views/programs/program_details_spec.js +++ /dev/null @@ -1,532 +0,0 @@ -define([ - 'jquery', - 'js/programs/collections/course_runs_collection', - 'js/programs/models/program_model', - 'js/programs/views/course_run_view', - 'js/programs/views/program_details_view', - 'js/programs/utils/constants' -], - function($, CourseRunsCollection, ProgramModel, CourseRunView, - ProgramDetailsView, constants) { - 'use strict'; - - describe('ProgramDetailsView', function() { - var view = {}, - model = {}, - courseRunsList = [ - { - id: 'course-v1:edX+DemoX+Demo_Course', - name: 'edX Demonstration Course', - number: 'DemoX', - org: 'edX', - short_description: null, - effort: null, - media: { - course_image: { - uri: '/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg' - }, - course_video: { - uri: null - } - }, - start: 'May 23, 2015', - start_type: 'timestamp', - start_display: 'Feb. 5, 2013', - end: null, - enrollment_start: null, - enrollment_end: null, - blocks_url: 'http://127.0.0.1:8000/api/courses/v1/blocks/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course' // eslint-disable-line max-len - }, - { - id: 'course-v1:edx+Krampus25+2015_12_05', - name: 'Krampusnacht', - number: 'Krampus25', - org: 'edx', - short_description: null, - effort: null, - media: { - course_image: { - uri: '/asset-v1:edx+Krampus25+2015_12_05+type@asset+block@images_course_image.jpg' - }, - course_video: { - uri: null - } - }, - start: '2030-01-01T00:00:00Z', - start_type: 'empty', - start_display: null, - end: null, - enrollment_start: null, - enrollment_end: null, - blocks_url: 'http://127.0.0.1:8000/api/courses/v1/blocks/?course_id=course-v1%3Aedx%2BKrampus25%2B2015_12_05' // eslint-disable-line max-len - }, - { - id: 'course-v1:edx+shiaLB101+2016_01', - name: 'Shia "The Beef"', - number: 'shiaLB101', - org: 'edx', - short_description: null, - effort: null, - media: { - course_image: { - uri: '/asset-v1:edx+shiaLB101+2016_01+type@asset+block@images_course_image.jpg' - }, - course_video: { - uri: null - } - }, - start: '2030-01-01T00:00:00Z', - start_type: 'empty', - start_display: null, - end: null, - enrollment_start: null, - enrollment_end: null, - blocks_url: 'http://127.0.0.1:8000/api/courses/v1/blocks/?course_id=course-v1%3Aedx%2BshiaLB101%2B2016_01' // eslint-disable-line max-len - } - ], - programData = { - category: 'XSeries', - course_codes: [{ - display_name: 'test-course-display_name', - key: 'test-course-key', - organization: { - display_name: 'test-org-display_name', - key: 'test-org-key' - }, - run_modes: [ - { - course_key: 'course-v1:edX+DemoX+Demo_Course', - mode_slug: 'honor', - sku: null, - start: 'May 23, 2015' - }, { - course_key: 'course-v1:edX+DemoX+Demo_Course', - mode_slug: 'honor', - sku: null, - start: 'August 01, 2015' - }, { - course_key: 'course-v1:edX+DemoX+Demo_Course', - mode_slug: 'honor', - sku: null, - start: 'December 11, 2015' - } - ] - }], - created: '2015-10-20T18:11:46.854451Z', - id: 5, - marketing_slug: 'test-program-slug', - modified: '2015-10-20T18:11:46.854735Z', - name: 'test-program-5', - organizations: [{ - display_name: 'test-org-display_name', - key: 'test-org-key' - }], - status: 'unpublished', - subtitle: 'test-subtitle' - }, - testTimeoutInterval = 100, - errorClass = 'has-error', - addCourse, - completeCourseForm, - dropdownSelect, - editField, - keyPress, - openPublishModal, - resetCourseCodes, - testHidingButtonsAfterPublish, - testInvalidUpdate, - testUnchangedFieldBlur, - testUpdatedFieldBlur; - - addCourse = function() { - var $addCourseBtn = view.$el.find('.js-add-course').first(), - $form, - $submitBtn; - - expect(view.$el.find('.course-details').length).toEqual(1); - $addCourseBtn.click(); - $form = view.$('.js-course-form'); - $submitBtn = $form.find('.js-select-course'); - completeCourseForm(); - $submitBtn.click(); - expect($form.find('.field-message.has-error').length).toEqual(0); - }; - - completeCourseForm = function() { - var $form = view.$('.js-course-form'); - - $form.find('.course-key').val('123'); - $form.find('.display-name').val('test course 1'); - }; - - dropdownSelect = function($select, value) { - $select.find('option:selected').prop('selected', false); - $select.val(value).prop('selected', true); - $select.trigger('change'); - }; - - editField = function(el, str) { - var $input = view.$el.find(el), - $btn = $input.next('.js-enable-edit'); - - expect(document.activeElement).not.toEqual($input[0]); - expect($input).not.toHaveClass('edit'); - expect($input).toHaveClass('is-hidden'); - - $btn.click(); - - $input.val(str); - - // Enable editing - expect($input).not.toHaveClass('is-hidden'); - expect($input).toHaveClass('edit'); - }; - - keyPress = function($el, key) { - $el.trigger({ - type: 'keydown', - keyCode: key, - which: key, - charCode: key - }); - }; - - openPublishModal = function() { - var $publishBtn = view.$el.find('.js-publish-program'), - defaultStatus = programData.status, - publishedStatus = 'active'; - - expect(view.modalView).not.toBeDefined(); - expect(view.model.get('status')).toEqual(defaultStatus); - expect(view.model.get('status')).not.toEqual(publishedStatus); - - $publishBtn.click(); - }; - - resetCourseCodes = function() { - var originalRun = programData.course_codes[0]; - - programData.course_codes = [originalRun]; - }; - - testUnchangedFieldBlur = function(el) { - var $input = view.$el.find(el), - $btn = view.$el.find('.js-add-course'), - title = $input.val(), - update = title; - - editField(el, update); - $btn.focus(); - $input.blur(); - - expect(title).toEqual(update); - expect(view.model.save).not.toHaveBeenCalled(); - }; - - testUpdatedFieldBlur = function(el, update) { - var $input = view.$el.find(el), - $btn = view.$el.find('.js-add-course'); - - expect($input.val()).not.toEqual(update); - - editField(el, update); - - $btn.focus(); - $input.blur(); - - expect($input.val()).toEqual(update); - expect(view.model.save).toHaveBeenCalled(); - }; - - testHidingButtonsAfterPublish = function(el) { - expect(view.$el.find(el).length).toBeGreaterThan(0); - view.model.set({status: 'active'}); - view.render(); - expect(view.$el.find(el).length).toEqual(0); - }; - - testInvalidUpdate = function(el, update) { - var $input = view.$el.find(el), - $btn = view.$el.find('.js-add-course'); - - editField(el, update); - - $btn.focus(); - $input.blur(); - - expect($input).toHaveClass(errorClass); - expect(view.model.save).not.toHaveBeenCalled(); - }; - - beforeEach(function() { - // Set the DOM - setFixtures(''); - - jasmine.clock().install(); - - spyOn(ProgramModel.prototype, 'set').and.callThrough(); - spyOn(ProgramModel.prototype, 'save'); - spyOn(CourseRunsCollection.prototype, 'fetch'); - - model = new ProgramModel(); - model.set(programData); - - view = new ProgramDetailsView({ - model: model - }); - - view.courseRuns.set(courseRunsList); - view.courseRuns.parse({results: courseRunsList}); - }); - - afterEach(function() { - resetCourseCodes(); - view.undelegateEvents(); - view.remove(); - - jasmine.clock().uninstall(); - }); - - describe('View data', function() { - it('should exist', function() { - expect(view).toBeDefined(); - }); - - it('should render all of the run_modes from the model', function() { - var $runs = view.$el.find('.js-course-runs'), - domLength = $runs.find('.js-remove-run').length, - objLength = programData.course_codes[0].run_modes.length; - - expect(domLength).toEqual(objLength); - }); - }); - - describe('Delete data', function() { - it('should remove a course when the delete button is clicked', function() { - var $el = view.$el.find('.js-course-list'), - $removeRunBtn = $el.find('.js-remove-course').first(), - count = programData.course_codes.length; - - expect($el.find('.js-remove-course').length).toEqual(count); - $removeRunBtn.click(); - - setTimeout(function() { - expect($el.find('.js-remove-course').length).toEqual(count - 1); - }, testTimeoutInterval); - - jasmine.clock().tick(testTimeoutInterval + 1); - }); - - it('should remove a course run when the delete button is clicked', function() { - var $runs = view.$el.find('.js-course-runs'), - $removeRunBtn = $runs.find('.js-remove-run').first(), - count = programData.course_codes[0].run_modes.length; - - expect($runs.find('.js-remove-run').length).toEqual(count); - $removeRunBtn.click(); - - setTimeout(function() { - expect($runs.find('.js-remove-run').length).toEqual(count - 1); - }, testTimeoutInterval); - - jasmine.clock().tick(testTimeoutInterval + 1); - }); - - it('should not show the delete course button if program status is not unpublished', function() { - testHidingButtonsAfterPublish('.js-remove-course'); - }); - - it('should not show the add course button if program status is not unpublished', function() { - testHidingButtonsAfterPublish('.js-add-course'); - }); - - it('should not show the delete run button if program status is not unpublished', function() { - testHidingButtonsAfterPublish('.js-remove-run'); - }); - }); - - describe('Add data', function() { - it('should add a new course details view on click of the add course button', function() { - var $btn = view.$el.find('.js-add-course').first(); - - expect(view.$('.js-course-form').length).toEqual(0); - $btn.click(); - expect(view.$('.js-course-form').length).toEqual(1); - }); - - it('should add a course when the form is submitted', function() { - addCourse(); - }); - - it('should not submit the course form when it is incomplete', function() { - var $addCourseBtn = view.$el.find('.js-add-course').first(), - $form, - $submitBtn; - - expect(view.$el.find('.course-details').length).toEqual(1); - $addCourseBtn.click(); - $form = view.$('.js-course-form'); - expect($form.find('.field-message.has-error').length).toEqual(0); - $submitBtn = $form.find('.js-select-course'); - $submitBtn.click(); - expect($form.find('.field-message.has-error').length).toEqual(2); - }); - - it('should allow a user to add a course run', function() { - var runSelect = '.js-course-run-select', - $runSelect, - savedRunCount = programData.course_codes[0].run_modes.length; - - addCourse(); - expect(view.$(runSelect).length).toEqual(0); - view.$('.js-add-course-run').first().click(); - - $runSelect = view.$(runSelect); - expect($runSelect.length).toEqual(1); - expect(view.$('.js-remove-run').length).toEqual(savedRunCount); - dropdownSelect($runSelect, courseRunsList[0].id); - expect(view.$(runSelect).length).toEqual(0); - expect(view.$('.js-remove-run').length).toEqual(savedRunCount + 1); - }); - - it('should update the course run dropdown if multiple are open and one is selected', function() { - var runSelect = '.js-course-run-select', - $addRunBtn, - $courseView, - courseRunOptionsCount = courseRunsList.length + 1; - - addCourse(); - expect(view.$(runSelect).length).toEqual(0); - - $courseView = view.$('.course-container').last(); - $addRunBtn = $courseView.find('.js-add-course-run'); - $addRunBtn.click(); - - expect(view.$(runSelect).length).toEqual(1); - expect(view.$(runSelect).find('option').length).toEqual(courseRunOptionsCount); - - $addRunBtn.click(); - expect(view.$(runSelect).length).toEqual(2); - - dropdownSelect(view.$(runSelect).first(), courseRunsList[0].id); - expect(view.$(runSelect).length).toEqual(1); - expect(view.$(runSelect).find('option').length).toEqual(courseRunOptionsCount - 1); - }); - }); - - describe('Edit data', function() { - it('should enable a user to edit the name, subtitle and marketing slug fields', function() { - editField('.program-name', 'name'); - editField('.program-subtitle', 'subtitle'); - editField('.program-marketing-slug', 'marketing-slug'); - }); - - it('should not send an API call if a user does not change the value of an editable field', function() { - testUnchangedFieldBlur('.program-name'); - testUnchangedFieldBlur('.program-subtitle'); - testUnchangedFieldBlur('.program-marketing-slug'); - }); - - it('should send an API call if a user changes the value of an editable field', function() { - testUpdatedFieldBlur('.program-name', 'new-title'); - testUpdatedFieldBlur('.program-subtitle', 'new-subtitle'); - testUpdatedFieldBlur('.program-marketing-slug', 'new-marketing-slug'); - }); - - it('should show error messaging if the updated required field is empty', function() { - testInvalidUpdate('.program-name', ''); - }); - - it('should show error messaging if the updated field value is too long', function() { - var chars256 = 'x'.repeat(256); - - testInvalidUpdate('.program-name', chars256); - testInvalidUpdate('.program-subtitle', chars256); - testInvalidUpdate('.program-marketing-slug', chars256); - }); - - it('should create a POST config object by default', function() { - var config = view.model.getConfig(); - - expect(config.type).toEqual('POST'); - expect(config.contentType).toEqual('application/json'); - expect(config.data).not.toBeDefined(); - }); - - it('should create a PATCH config object when passed in object sets patch as true', function() { - var data = {name: 'patched name'}, - config = view.model.getConfig({ - patch: true, - update: data - }); - - expect(config.type).toEqual('PATCH'); - expect(config.contentType).toEqual('application/merge-patch+json'); - expect(config.data).toBeDefined(); - expect(config.data).toEqual(JSON.stringify(data)); - }); - }); - - describe('Publish a Program', function() { - it('should open the publish modal when the publish button is clicked', function() { - openPublishModal(); - expect(view.modalView).toBeDefined(); - }); - - it('should publish a program when the publish confirm button is clicked', function() { - var defaultStatus = programData.status, - publishedStatus = 'active'; - - openPublishModal(); - expect(view.modalView).toBeDefined(); - - view.$el.find('.js-confirm').click(); - - // Model should be set and save called - expect(view.model.set).toHaveBeenCalled(); - expect(view.model.get('status')).not.toEqual(defaultStatus); - expect(view.model.get('status')).toEqual(publishedStatus); - expect(view.model.save).toHaveBeenCalled(); - - // Publish button should be removed once API has completed its call - expect(view.$el.find('.js-publish-program').length).toEqual(1); - view.model.trigger('sync'); - expect(view.$el.find('.js-publish-program').length).toEqual(0); - }); - - it('should show a validation error when publish button pressed if validation fails', function() { - var $input = view.$el.find('#program-marketing-slug'); - - view.model.set('marketing_slug', ''); - openPublishModal(); - expect(view.modalView).not.toBeDefined(); - expect($input).toHaveClass(errorClass); - }); - - it('should destroy the publish modal when the cancel button is clicked', function() { - openPublishModal(); - expect(view.modalView).toBeDefined(); - - // Close the modal - view.$el.find('.js-cancel').click(); - - // Expect the modal DOM elements to not be there anymore - expect(view.$el.find('.js-cancel').length).toEqual(0); - expect(view.modalView.$parentEl.html().length).toEqual(0); - }); - - it('should destroy the publish modal when the esc key is pressed', function() { - openPublishModal(); - expect(view.modalView).toBeDefined(); - - // Close the modal - keyPress(view.modalView.$el, constants.keyCodes.esc); - - // Expect the modal DOM elements to not be there anymore - expect(view.$el.find('.js-cancel').length).toEqual(0); - expect(view.modalView.$parentEl.html().length).toEqual(0); - }); - }); - }); - } -); diff --git a/cms/static/sass/_build-v2.scss b/cms/static/sass/_build-v2.scss index 7335783fbd..0aecd11645 100644 --- a/cms/static/sass/_build-v2.scss +++ b/cms/static/sass/_build-v2.scss @@ -16,4 +16,3 @@ @import 'elements/footer'; @import 'elements-v2/sock'; @import 'elements-v2/tooltip'; -@import 'programs/build'; diff --git a/cms/static/sass/programs/_app-container.scss b/cms/static/sass/programs/_app-container.scss deleted file mode 100644 index cd5fadef67..0000000000 --- a/cms/static/sass/programs/_app-container.scss +++ /dev/null @@ -1,15 +0,0 @@ - -// ------------------------------ -// Programs: App Container - -// About: styling for setting up the wrapper. -.program-app { - &.layout-1q3q { - max-width: 1250px; - - // HtmlUtils.template wraps children of this in an anonymous div. We need to make sure they render at full width - & > div { - @include span(12); - } - } -} diff --git a/cms/static/sass/programs/_build.scss b/cms/static/sass/programs/_build.scss deleted file mode 100644 index 272dcbe63f..0000000000 --- a/cms/static/sass/programs/_build.scss +++ /dev/null @@ -1,9 +0,0 @@ -// ------------------------------ -// Programs: Main Style Compile - -// About: Sass compile for the Programs IDA. - -@import 'components'; -@import 'views'; -@import 'modals'; -@import 'app-container'; diff --git a/cms/static/sass/programs/_components.scss b/cms/static/sass/programs/_components.scss deleted file mode 100644 index c14957880f..0000000000 --- a/cms/static/sass/programs/_components.scss +++ /dev/null @@ -1,99 +0,0 @@ -// ------------------------------ -// Programs: Components - -// About: styling for specific UI components ranging from global to modular. - -// #BUTTONS -// #FORMS - - -// ------------------------------ -// #BUTTONS -// ------------------------------ -.btn { - &.btn-delete, - &.btn-edit { - border: none; - background: none; - color: palette(grayscale, base); - - &:hover, - &:focus, - &:active { - color: $black; - } - } - - &.full { - width: 100%; - } - - &.right { - @include float(right); - } - - &.btn-create { - background: palette(success, base); - border-color: palette(success, base); - - // STATE: hover and focus - &:hover, - &.is-hovered, - &:focus, - &.is-focused { - background: shade($success, 33%); - color: palette(primary, accent); - } - - // STATE: is pressed or active - &:active, - &.is-pressed, - &.is-active { - border-color: shade($success, 33%); - background: shade($success, 33%); - } - - .text { - margin-left: 5px; - } - } - - .icon, - .text { - vertical-align: middle; - } - - .icon { - font-size: 16px; - } -} - -// ------------------------------ -// #FORMS -// ------------------------------ -.field { - .invalid { - border: 2px solid palette(error, base); - } - - .field-input, - .field-hint, - .field-message { - min-with: 300px; - width: 50%; - - &.is-hidden { - @extend .is-hidden; - } - } - - .copy { - vertical-align: middle; - } -} - -.form-group { - &.bg-white { - background-color: $white; - } -} diff --git a/cms/static/sass/programs/_modals.scss b/cms/static/sass/programs/_modals.scss deleted file mode 100644 index 2e53637776..0000000000 --- a/cms/static/sass/programs/_modals.scss +++ /dev/null @@ -1,77 +0,0 @@ -// ------------------------------ -// Programs: Modals - -// About: styling for modals. -.modal-window-overlay { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: palette(grayscale, dark); - opacity: 0.5; - z-index: 1000; -} - -.modal-window { - position: absolute; - background-color: $black; - width: 80%; - left: 10%; - top: 40%; - z-index: 1001; -} - -.modal-content { - margin: 5px; - padding: 20px; - background-color: palette(grayscale, dark); - border-top: 5px solid palette(warning, accent); - - .copy { - color: $white; - } - - .emphasized { - color: $white; - font-weight: font-weight(bold); - } -} - -.modal-actions { - padding: 10px 20px; - - .btn { - color: palette(grayscale, back); - } - - .btn-brand { - background: palette(warning, back); - color: palette(grayscale, dark); - border-color: palette(warning, accent); - - &:hover, - &:focus, - &:active { - background: palette(warning, back); - border-color: palette(warning, accent); - } - } - - .btn-neutral { - background: transparent; - border-color: transparent; - &:hover, - &:focus, - &:active { - border-color: palette(grayscale, back) - } - } -} - -@include breakpoint( $bp-screen-sm ) { - .modal-window { - width: 440px; - left: calc( 50% - 220px ); - } -} diff --git a/cms/static/sass/programs/_views.scss b/cms/static/sass/programs/_views.scss deleted file mode 100644 index 741d19346a..0000000000 --- a/cms/static/sass/programs/_views.scss +++ /dev/null @@ -1,67 +0,0 @@ - -// ------------------------------ -// Programs: Views - -// About: styling for specific views. - -// ------------------------------ -// #PROGRAM LISTS -// ------------------------------ -.program-list { - list-style-type: none; - padding-left: 0; - - .program-details { - .name { - font-size: 2rem; - } - - .status { - @include float(right); - } - - .category { - color: palette(grayscale, base); - } - } -} - -.app-header { - @include clearfix(); - @include grid-row; - border-bottom: 1px solid palette(grayscale, base); - margin-bottom: 20px; - - form { - @include span(12); - } -} - -.course-container { - .subtitle { - color: palette(grayscale, base); - } -} - -.run-container { - position: relative; - margin: { - bottom: 20px; - }; - - &:before { - content: ''; - width: 5px; - height: calc( 100% + 1px ); - background: palette(grayscale, base); - position: absolute; - top: 0; - left: 0; - } -} - -.course-container { - margin: { - bottom: 20px; - }; -} diff --git a/cms/static/sass/studio-main-v2.scss b/cms/static/sass/studio-main-v2.scss index f31bcb3817..cd355d5a7d 100644 --- a/cms/static/sass/studio-main-v2.scss +++ b/cms/static/sass/studio-main-v2.scss @@ -12,4 +12,3 @@ $pattern-library-path: '../edx-pattern-library' !default; // Load the shared build @import 'build-v2'; -@import 'programs/build'; diff --git a/cms/static/sass/views/_dashboard.scss b/cms/static/sass/views/_dashboard.scss index 1473d8cd67..dc53703876 100644 --- a/cms/static/sass/views/_dashboard.scss +++ b/cms/static/sass/views/_dashboard.scss @@ -55,7 +55,7 @@ } - .action-create-course, .action-create-library, .action-create-program { + .action-create-course, .action-create-library { @extend %btn-primary-green; @extend %t-action3; } @@ -318,7 +318,7 @@ } // ELEM: course listings - .courses-tab, .libraries-tab, .programs-tab { + .courses-tab, .libraries-tab { display: none; &.active { @@ -326,7 +326,7 @@ } } - .courses, .libraries, .programs { + .courses, .libraries { .title { @extend %t-title6; margin-bottom: $baseline; diff --git a/cms/templates/index.html b/cms/templates/index.html index daf518d8a6..ccf85c37f5 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -38,11 +38,6 @@ from openedx.core.djangolib.markup import HTML, Text ${_("New Library")} % endif - - % if is_programs_enabled: - - ${_("New Program")} - % endif @@ -295,17 +290,10 @@ from openedx.core.djangolib.markup import HTML, Text %endif - % if libraries_enabled or is_programs_enabled: + % if libraries_enabled:${_("Programs are groups of courses related to a common subject.")}
-