diff --git a/.eslintignore b/.eslintignore index 77ab0c22a5..120dab4bc1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -55,3 +55,6 @@ common/lib/xmodule/xmodule/js/src/sequence/display.js common/lib/xmodule/xmodule/js/src/sequence/edit.js common/lib/xmodule/xmodule/js/src/tabs/tabs-aggregator.js common/lib/xmodule/xmodule/js/src/vertical/edit.js + +# This file is responsible for almost half of the repo's total issues. +common/lib/xmodule/xmodule/js/src/capa/schematic.js diff --git a/.gitignore b/.gitignore index 1b55818343..4be2989f47 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,7 @@ cms/static/sass/*.css cms/static/sass/*.css.map cms/static/themed_sass/ themes/**/css/*.css +themes/**/css/discussion/*.css ### Logging artifacts log/ diff --git a/cms/djangoapps/contentstore/features/course-updates.feature b/cms/djangoapps/contentstore/features/course-updates.feature index 79d6445194..f71d6c3d78 100644 --- a/cms/djangoapps/contentstore/features/course-updates.feature +++ b/cms/djangoapps/contentstore/features/course-updates.feature @@ -2,46 +2,6 @@ Feature: CMS.Course updates As a course author, I want to be able to provide updates to my students -# Commenting out as flaky TNL-5051 07/20/2016 - # Internet explorer can't select all so the update appears weirdly -# @skip_internetexplorer -# Scenario: Users can add updates -# Given I have opened a new course in Studio -# And I go to the course updates page -# When I add a new update with the text "Hello" -# Then I should see the update "Hello" -# And I see a "saving" notification - -# Commenting out as flaky TNL-5051 07/20/2016 -# # Internet explorer can't select all so the update appears weirdly -# @skip_internetexplorer -# Scenario: Users can edit updates -# Given I have opened a new course in Studio -# And I go to the course updates page -# When I add a new update with the text "Hello" -# And I modify the text to "Goodbye" -# Then I should see the update "Goodbye" -# And I see a "saving" notification - -# Commenting out as flaky TNL-5051 07/20/2016 -# Scenario: Users can delete updates -# Given I have opened a new course in Studio -# And I go to the course updates page -# And I add a new update with the text "Hello" -# And I delete the update -# And I confirm the prompt -# Then I should not see the update "Hello" -# And I see a "deleting" notification - -# Commenting out as flaky TNL-5051 07/20/2016 -# Scenario: Users can edit update dates -# Given I have opened a new course in Studio -# And I go to the course updates page -# And I add a new update with the text "Hello" -# When I edit the date to "06/01/13" -# Then I should see the date "June 1, 2013" -# And I see a "saving" notification - # Internet explorer can't select all so the update appears weirdly @skip_internetexplorer Scenario: Users can change handouts @@ -51,26 +11,6 @@ Feature: CMS.Course updates Then I see the handout "Test" And I see a "saving" notification - Scenario: Text outside of tags is preserved - Given I have opened a new course in Studio - And I go to the course updates page - When I add a new update with the text "before middle after" - Then I should see the update "before middle after" - And when I reload the page - Then I should see the update "before middle after" - -# Commenting out as flaky TNL-5051 07/22/2016 -# Scenario: Static links are rewritten when previewing a course update -# Given I have opened a new course in Studio -# And I go to the course updates page -# When I add a new update with the text "" -# # Can only do partial text matches because of the quotes with in quotes (and regexp step matching). -# Then I should see the asset update to "my_img.jpg" -# And I change the update from "/static/my_img.jpg" to "" -# Then I should see the asset update to "modified.jpg" -# And when I reload the page -# Then I should see the asset update to "modified.jpg" - Scenario: Static links are rewritten when previewing handouts Given I have opened a new course in Studio And I go to the course updates page diff --git a/cms/djangoapps/contentstore/features/course-updates.py b/cms/djangoapps/contentstore/features/course-updates.py index 1b50910a65..3a9d0103c6 100644 --- a/cms/djangoapps/contentstore/features/course-updates.py +++ b/cms/djangoapps/contentstore/features/course-updates.py @@ -1,8 +1,7 @@ # pylint: disable=missing-docstring +from cms.djangoapps.contentstore.features.common import type_in_codemirror, get_codemirror_value from lettuce import world, step -from selenium.webdriver.common.keys import Keys -from common import type_in_codemirror, get_codemirror_value from nose.tools import assert_in @@ -15,77 +14,11 @@ def go_to_updates(_step): world.wait_for_visible('#course-handouts-view') -@step(u'I add a new update with the text "([^"]*)"$') -def add_update(_step, text): - update_css = '.new-update-button' - world.css_click(update_css) - world.wait_for_visible('.CodeMirror') - change_text(text) - - -@step(u'I should see the update "([^"]*)"$') -def check_update(_step, text): - update_css = 'div.update-contents' - update_html = world.css_find(update_css).html - assert_in(text, update_html) - - -@step(u'I should see the asset update to "([^"]*)"$') -def check_asset_update(_step, asset_file): - update_css = 'div.update-contents' - update_html = world.css_find(update_css).html - asset_key = world.scenario_dict['COURSE'].id.make_asset_key(asset_type='asset', path=asset_file) - assert_in(unicode(asset_key), update_html) - - -@step(u'I should not see the update "([^"]*)"$') -def check_no_update(_step, text): - update_css = 'div.update-contents' - assert world.is_css_not_present(update_css) - - -@step(u'I modify the text to "([^"]*)"$') -def modify_update(_step, text): - button_css = 'div.post-preview .edit-button' - world.css_click(button_css) - change_text(text) - - -@step(u'I change the update from "([^"]*)" to "([^"]*)"$') -def change_existing_update(_step, before, after): - verify_text_in_editor_and_update('div.post-preview .edit-button', before, after) - - @step(u'I change the handout from "([^"]*)" to "([^"]*)"$') def change_existing_handout(_step, before, after): verify_text_in_editor_and_update('div.course-handouts .edit-button', before, after) -@step(u'I delete the update$') -def click_button(_step): - button_css = 'div.post-preview .delete-button' - world.css_click(button_css) - - -@step(u'I edit the date to "([^"]*)"$') -def change_date(_step, new_date): - button_css = 'div.post-preview .edit-button' - world.css_click(button_css) - date_css = 'input.date' - date = world.css_find(date_css) - for __ in range(len(date.value)): - date._element.send_keys(Keys.END, Keys.BACK_SPACE) - date._element.send_keys(new_date) - save_css = '.save-button' - world.css_click(save_css) - - -@step(u'I should see the date "([^"]*)"$') -def check_date(_step, date): - date_css = 'span.date-display' - assert_in(date, world.css_html(date_css)) - - @step(u'I modify the handout to "([^"]*)"$') def edit_handouts(_step, text): edit_css = 'div.course-handouts > .edit-button' diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py b/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py index 32fff608a4..02f37e8b16 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_create_course.py @@ -61,3 +61,35 @@ class TestCreateCourse(ModuleStoreTestCase): ) # pylint: disable=protected-access self.assertEqual(store, modulestore()._get_modulestore_for_courselike(new_key).get_modulestore_type()) + + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) + def test_get_course_with_different_case(self, default_store): + """ + Tests that course can not be accessed with different case. + + Scenario: + Create a course with lower case keys inside `bulk_operations` with `ignore_case=True`. + Verify that course is created. + Verify that get course from store using same course id but different case is not accessible. + """ + org = 'org1' + number = 'course1' + run = 'run1' + with self.store.default_store(default_store): + lowercase_course_id = self.store.make_course_key(org, number, run) + with self.store.bulk_operations(lowercase_course_id, ignore_case=True): + # Create course with lowercase key & Verify that store returns course. + self.store.create_course( + lowercase_course_id.org, + lowercase_course_id.course, + lowercase_course_id.run, + self.user.id + ) + course = self.store.get_course(lowercase_course_id) + self.assertIsNotNone(course, 'Course not found using lowercase course key.') + self.assertEqual(unicode(course.id), unicode(lowercase_course_id)) + + # Verify store does not return course with different case. + uppercase_course_id = self.store.make_course_key(org.upper(), number.upper(), run.upper()) + course = self.store.get_course(uppercase_course_id) + self.assertIsNone(course, 'Course should not be accessed with uppercase course id.') diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py index 1d83b52a02..921ca75507 100644 --- a/cms/djangoapps/contentstore/tests/test_contentstore.py +++ b/cms/djangoapps/contentstore/tests/test_contentstore.py @@ -1151,6 +1151,9 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin): """ Tests for the CMS ContentStore application. """ + duplicate_course_error = ("There is already a course defined with the same organization and course number. " + "Please change either organization or course number to be unique.") + def setUp(self): super(ContentStoreTest, self).setUp() @@ -1203,6 +1206,22 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin): self.course_data['run'] = 'run.name' self.assert_created_course() + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) + def test_course_with_different_cases(self, default_store): + """ + Tests that course can not be created with different case using an AJAX request to + course handler. + """ + course_number = '99x' + with self.store.default_store(default_store): + # Verify create a course passes with lower case. + self.course_data['number'] = course_number.lower() + self.assert_created_course() + + # Verify create a course fail when same course number is provided with different case. + self.course_data['number'] = course_number.upper() + self.assert_course_creation_failed(self.duplicate_course_error) + def test_create_course_check_forum_seeding(self): """Test new course creation and verify forum seeding """ test_course_data = self.assert_created_course(number_suffix=uuid4().hex) @@ -1289,7 +1308,7 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin): def test_create_course_duplicate_course(self): """Test new course creation - error path""" self.client.ajax_post('/course/', self.course_data) - self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change either organization or course number to be unique.') + self.assert_course_creation_failed(self.duplicate_course_error) def assert_course_creation_failed(self, error_message): """ @@ -1318,21 +1337,38 @@ class ContentStoreTest(ContentStoreTestCase, XssTestMixin): self.course_data['display_name'] = 'Robot Super Course Two' self.course_data['run'] = '2013_Summer' - self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change either organization or course number to be unique.') + self.assert_course_creation_failed(self.duplicate_course_error) - def test_create_course_case_change(self): + @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) + def test_create_course_case_change(self, default_store): """Test new course creation - error path due to case insensitive name equality""" - self.course_data['number'] = 'capital' - self.client.ajax_post('/course/', self.course_data) - cache_current = self.course_data['org'] - self.course_data['org'] = self.course_data['org'].lower() - self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change either organization or course number to be unique.') - self.course_data['org'] = cache_current + self.course_data['number'] = '99x' - self.client.ajax_post('/course/', self.course_data) - cache_current = self.course_data['number'] - self.course_data['number'] = self.course_data['number'].upper() - self.assert_course_creation_failed('There is already a course defined with the same organization and course number. Please change either organization or course number to be unique.') + with self.store.default_store(default_store): + + # Verify that the course was created properly. + self.assert_created_course() + + # Keep the copy of original org + cache_current = self.course_data['org'] + + # Change `org` to lower case and verify that course did not get created + self.course_data['org'] = self.course_data['org'].lower() + self.assert_course_creation_failed(self.duplicate_course_error) + + # Replace the org with its actual value, and keep the copy of course number. + self.course_data['org'] = cache_current + cache_current = self.course_data['number'] + + self.course_data['number'] = self.course_data['number'].upper() + self.assert_course_creation_failed(self.duplicate_course_error) + + # Replace the org with its actual value, and keep the copy of course number. + self.course_data['number'] = cache_current + __ = self.course_data['run'] + + self.course_data['run'] = self.course_data['run'].upper() + self.assert_course_creation_failed(self.duplicate_course_error) def test_course_substring(self): """ diff --git a/cms/djangoapps/contentstore/views/program.py b/cms/djangoapps/contentstore/views/program.py index d95f10b9c5..0ac51d5df0 100644 --- a/cms/djangoapps/contentstore/views/program.py +++ b/cms/djangoapps/contentstore/views/program.py @@ -16,9 +16,8 @@ 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 maintained in a separate repository. - The app handles its own routing and provides a UI which can be used to create and - publish new Programs (e.g, XSeries). + 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) diff --git a/cms/djangoapps/contentstore/views/tests/test_certificates.py b/cms/djangoapps/contentstore/views/tests/test_certificates.py index 4958b40643..49e62c25ee 100644 --- a/cms/djangoapps/contentstore/views/tests/test_certificates.py +++ b/cms/djangoapps/contentstore/views/tests/test_certificates.py @@ -26,7 +26,7 @@ from course_modes.tests.factories import CourseModeFactory from contentstore.views.certificates import CertificateManager from django.test.utils import override_settings from contentstore.utils import get_lms_link_for_certificate_web_view -from util.testing import EventTestMixin +from util.testing import EventTestMixin, UrlResetMixin FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True @@ -197,7 +197,9 @@ class CertificatesBaseTestCase(object): @ddt.ddt @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) -class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods): +class CertificatesListHandlerTestCase( + EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods, UrlResetMixin +): """ Test cases for certificates_list_handler. """ @@ -206,6 +208,7 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat Set up CertificatesListHandlerTestCase. """ super(CertificatesListHandlerTestCase, self).setUp('contentstore.views.certificates.tracker') + self.reset_urls() def _url(self): """ @@ -420,7 +423,9 @@ class CertificatesListHandlerTestCase(EventTestMixin, CourseTestCase, Certificat @ddt.ddt @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) -class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods): +class CertificatesDetailHandlerTestCase( + EventTestMixin, CourseTestCase, CertificatesBaseTestCase, HelperMethods, UrlResetMixin +): """ Test cases for CertificatesDetailHandlerTestCase. """ @@ -432,6 +437,7 @@ class CertificatesDetailHandlerTestCase(EventTestMixin, CourseTestCase, Certific Set up CertificatesDetailHandlerTestCase. """ super(CertificatesDetailHandlerTestCase, self).setUp('contentstore.views.certificates.tracker') + self.reset_urls() def _url(self, cid=-1): """ diff --git a/cms/djangoapps/contentstore/views/tests/test_header_menu.py b/cms/djangoapps/contentstore/views/tests/test_header_menu.py index c27048452a..a854e33d6e 100644 --- a/cms/djangoapps/contentstore/views/tests/test_header_menu.py +++ b/cms/djangoapps/contentstore/views/tests/test_header_menu.py @@ -8,13 +8,14 @@ from django.test.utils import override_settings from contentstore.tests.utils import CourseTestCase from contentstore.utils import reverse_course_url +from util.testing import UrlResetMixin FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True @override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED) -class TestHeaderMenu(CourseTestCase): +class TestHeaderMenu(CourseTestCase, UrlResetMixin): """ Unit tests for the course header menu. """ @@ -23,6 +24,7 @@ class TestHeaderMenu(CourseTestCase): Set up the for the course header menu tests. """ super(TestHeaderMenu, self).setUp() + self.reset_urls() def test_header_menu_without_web_certs_enabled(self): """ diff --git a/cms/djangoapps/maintenance/__init__.py b/cms/djangoapps/maintenance/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cms/djangoapps/maintenance/tests.py b/cms/djangoapps/maintenance/tests.py new file mode 100644 index 0000000000..696c703020 --- /dev/null +++ b/cms/djangoapps/maintenance/tests.py @@ -0,0 +1,248 @@ +""" +Tests for the maintenance app views. +""" +import ddt +import json + +from django.conf import settings +from django.core.urlresolvers import reverse + +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory + + +from contentstore.management.commands.utils import get_course_versions +from student.tests.factories import AdminFactory, UserFactory + +from .views import COURSE_KEY_ERROR_MESSAGES, MAINTENANCE_VIEWS + + +# This list contains URLs of all maintenance app views. +MAINTENANCE_URLS = [reverse(view['url']) for view in MAINTENANCE_VIEWS.values()] + + +class TestMaintenanceIndex(ModuleStoreTestCase): + """ + Tests for maintenance index view. + """ + + def setUp(self): + super(TestMaintenanceIndex, self).setUp() + self.user = AdminFactory() + login_success = self.client.login(username=self.user.username, password='test') + self.assertTrue(login_success) + self.view_url = reverse('maintenance:maintenance_index') + + def test_maintenance_index(self): + """ + Test that maintenance index view lists all the maintenance app views. + """ + response = self.client.get(self.view_url) + self.assertContains(response, 'Maintenance', status_code=200) + + # Check that all the expected links appear on the index page. + for url in MAINTENANCE_URLS: + self.assertContains(response, url, status_code=200) + + +@ddt.ddt +class MaintenanceViewTestCase(ModuleStoreTestCase): + """ + Base class for maintenance view tests. + """ + view_url = '' + + def setUp(self): + super(MaintenanceViewTestCase, self).setUp() + self.user = AdminFactory() + login_success = self.client.login(username=self.user.username, password='test') + self.assertTrue(login_success) + + def verify_error_message(self, data, error_message): + """ + Verify the response contains error message. + """ + response = self.client.post(self.view_url, data=data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertContains(response, error_message, status_code=200) + + def tearDown(self): + """ + Reverse the setup. + """ + self.client.logout() + super(MaintenanceViewTestCase, self).tearDown() + + +@ddt.ddt +class MaintenanceViewAccessTests(MaintenanceViewTestCase): + """ + Tests for access control of maintenance views. + """ + @ddt.data(MAINTENANCE_URLS) + @ddt.unpack + def test_require_login(self, url): + """ + Test that maintenance app requires user login. + """ + # Log out then try to retrieve the page + self.client.logout() + response = self.client.get(url) + + # Expect a redirect to the login page + redirect_url = '{login_url}?next={original_url}'.format( + login_url=reverse('login'), + original_url=url, + ) + + self.assertRedirects(response, redirect_url) + + @ddt.data(MAINTENANCE_URLS) + @ddt.unpack + def test_global_staff_access(self, url): + """ + Test that all maintenance app views are accessible to global staff user. + """ + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + @ddt.data(MAINTENANCE_URLS) + @ddt.unpack + def test_non_global_staff_access(self, url): + """ + Test that all maintenance app views are not accessible to non-global-staff user. + """ + user = UserFactory(username='test', email='test@example.com', password='test') + login_success = self.client.login(username=user.username, password='test') + self.assertTrue(login_success) + + response = self.client.get(url) + self.assertContains( + response, + 'Must be {platform_name} staff to perform this action.'.format(platform_name=settings.PLATFORM_NAME), + status_code=403 + ) + + +@ddt.ddt +class TestForcePublish(MaintenanceViewTestCase): + """ + Tests for the force publish view. + """ + + def setUp(self): + super(TestForcePublish, self).setUp() + self.view_url = reverse('maintenance:force_publish_course') + + def setup_test_course(self): + """ + Creates the course and add some changes to it. + + Returns: + course: a course object + """ + course = CourseFactory.create(default_store=ModuleStoreEnum.Type.split) + # Add some changes to course + chapter = ItemFactory.create(category='chapter', parent_location=course.location) + self.store.create_child( + self.user.id, # pylint: disable=no-member + chapter.location, + 'html', + block_id='html_component' + ) + # verify that course has changes. + self.assertTrue(self.store.has_changes(self.store.get_item(course.location))) + return course + + @ddt.data( + ('', COURSE_KEY_ERROR_MESSAGES['empty_course_key']), + ('edx', COURSE_KEY_ERROR_MESSAGES['invalid_course_key']), + ('course-v1:e+d+X', COURSE_KEY_ERROR_MESSAGES['course_key_not_found']), + ) + @ddt.unpack + def test_invalid_course_key_messages(self, course_key, error_message): + """ + Test all error messages for invalid course keys. + """ + # validate that course key contains error message + self.verify_error_message( + data={'course-id': course_key}, + error_message=error_message + ) + + def test_mongo_course(self): + """ + Test that we get a error message on old mongo courses. + """ + # validate non split error message + course = CourseFactory.create(default_store=ModuleStoreEnum.Type.mongo) + self.verify_error_message( + data={'course-id': unicode(course.id)}, + error_message='Force publishing course is not supported with old mongo courses.' + ) + + def test_already_published(self): + """ + Test that when a course is forcefully publish, we get a 'course is already published' message. + """ + course = self.setup_test_course() + + # publish the course + source_store = modulestore()._get_modulestore_for_courselike(course.id) # pylint: disable=protected-access + source_store.force_publish_course(course.id, self.user.id, commit=True) # pylint: disable=no-member + + # now course is published, we should get `already published course` error. + self.verify_error_message( + data={'course-id': unicode(course.id)}, + error_message='Course is already in published state.' + ) + + def verify_versions_are_different(self, course): + """ + Verify draft and published versions point to different locations. + + Arguments: + course (object): a course object. + """ + # get draft and publish branch versions + versions = get_course_versions(unicode(course.id)) + + # verify that draft and publish point to different versions + self.assertNotEqual(versions['draft-branch'], versions['published-branch']) + + def get_force_publish_course_response(self, course): + """ + Get force publish the course response. + + Arguments: + course (object): a course object. + + Returns: + response : response from force publish post view. + """ + # Verify versions point to different locations initially + self.verify_versions_are_different(course) + + # force publish course view + data = { + 'course-id': unicode(course.id) + } + response = self.client.post(self.view_url, data=data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + response_data = json.loads(response.content) + return response_data + + def test_force_publish_dry_run(self): + """ + Test that dry run does not publishes the course but shows possible outcome if force published is executed. + """ + course = self.setup_test_course() + response = self.get_force_publish_course_response(course) + + self.assertIn('current_versions', response) + + # verify that course still has changes as we just dry ran force publish course. + self.assertTrue(self.store.has_changes(self.store.get_item(course.location))) + + # verify that both branch versions are still different + self.verify_versions_are_different(course) diff --git a/cms/djangoapps/maintenance/urls.py b/cms/djangoapps/maintenance/urls.py new file mode 100644 index 0000000000..99ee2bc40d --- /dev/null +++ b/cms/djangoapps/maintenance/urls.py @@ -0,0 +1,13 @@ +""" +URLs for the maintenance app. +""" +from django.conf.urls import patterns, url + +from .views import MaintenanceIndexView, ForcePublishCourseView + + +urlpatterns = patterns( + '', + url(r'^$', MaintenanceIndexView.as_view(), name='maintenance_index'), + url(r'^force_publish_course/?$', ForcePublishCourseView.as_view(), name='force_publish_course'), +) diff --git a/cms/djangoapps/maintenance/views.py b/cms/djangoapps/maintenance/views.py new file mode 100644 index 0000000000..b370d4bc5c --- /dev/null +++ b/cms/djangoapps/maintenance/views.py @@ -0,0 +1,213 @@ +""" +Views for the maintenance app. +""" +import logging +from django.db import transaction +from django.core.validators import ValidationError +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.generic import View + +from edxmako.shortcuts import render_to_response +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError + +from contentstore.management.commands.utils import get_course_versions +from util.json_request import JsonResponse +from util.views import require_global_staff + + +log = logging.getLogger(__name__) + +# This dict maintains all the views that will be used Maintenance app. +MAINTENANCE_VIEWS = { + 'force_publish_course': { + 'url': 'maintenance:force_publish_course', + 'name': _('Force Publish Course'), + 'slug': 'force_publish_course', + 'description': _( + 'Sometimes the draft and published branches of a course can get out of sync. Force publish course command ' + 'resets the published branch of a course to point to the draft branch, effectively force publishing the ' + 'course. This view dry runs the force publish command' + ), + }, +} + + +COURSE_KEY_ERROR_MESSAGES = { + 'empty_course_key': _('Please provide course id.'), + 'invalid_course_key': _('Invalid course key.'), + 'course_key_not_found': _('No matching course found.') +} + + +class MaintenanceIndexView(View): + """ + Index view for maintenance dashboard, used by global staff. + + This view lists some commands/tasks that can be used to dry run or execute directly. + """ + + @method_decorator(require_global_staff) + def get(self, request): + """Render the maintenance index view. """ + return render_to_response('maintenance/index.html', { + 'views': MAINTENANCE_VIEWS, + }) + + +class MaintenanceBaseView(View): + """ + Base class for Maintenance views. + """ + + template = 'maintenance/container.html' + + def __init__(self, view=None): + self.context = { + 'view': view if view else '', + 'form_data': {}, + 'error': False, + 'msg': '' + } + + def render_response(self): + """ + A short method to render_to_response that renders response. + """ + if self.request.is_ajax(): + return JsonResponse(self.context) + return render_to_response(self.template, self.context) + + @method_decorator(require_global_staff) + def get(self, request): + """ + Render get view. + """ + return self.render_response() + + def validate_course_key(self, course_key, branch=ModuleStoreEnum.BranchName.draft): + """ + Validates the course_key that would be used by maintenance app views. + + Arguments: + course_key (string): a course key + branch: a course locator branch, default value is ModuleStoreEnum.BranchName.draft . + values can be either ModuleStoreEnum.BranchName.draft or ModuleStoreEnum.BranchName.published. + + Returns: + course_usage_key (CourseLocator): course usage locator + """ + if not course_key: + raise ValidationError(COURSE_KEY_ERROR_MESSAGES['empty_course_key']) + + course_usage_key = CourseKey.from_string(course_key) + + if not modulestore().has_course(course_usage_key): + raise ItemNotFoundError(COURSE_KEY_ERROR_MESSAGES['course_key_not_found']) + + # get branch specific locator + course_usage_key = course_usage_key.for_branch(branch) + + return course_usage_key + + +class ForcePublishCourseView(MaintenanceBaseView): + """ + View for force publishing state of the course, used by the global staff. + + This view uses `force_publish_course` method of modulestore which publishes the draft state of the course. After + the course has been forced published, both draft and publish draft point to same location. + """ + + def __init__(self): + super(ForcePublishCourseView, self).__init__(MAINTENANCE_VIEWS['force_publish_course']) + self.context.update({ + 'current_versions': [], + 'updated_versions': [], + 'form_data': { + 'course_id': '', + 'is_dry_run': True + } + }) + + def get_course_branch_versions(self, versions): + """ + Returns a dict containing unicoded values of draft and published draft versions. + """ + return { + 'draft-branch': unicode(versions['draft-branch']), + 'published-branch': unicode(versions['published-branch']) + } + + @transaction.atomic + @method_decorator(require_global_staff) + def post(self, request): + """ + This method force publishes a course if dry-run argument is not selected. If dry-run is selected, this view + shows possible outcome if the `force_publish_course` modulestore method is executed. + + Arguments: + course_id (string): a request parameter containing course id + is_dry_run (string): a request parameter containing dry run value. + It is obtained from checkbox so it has either values 'on' or ''. + """ + course_id = request.POST.get('course-id') + + self.context.update({ + 'form_data': { + 'course_id': course_id + } + }) + + try: + course_usage_key = self.validate_course_key(course_id) + except InvalidKeyError: + self.context['error'] = True + self.context['msg'] = COURSE_KEY_ERROR_MESSAGES['invalid_course_key'] + except ItemNotFoundError as exc: + self.context['error'] = True + self.context['msg'] = exc.message + except ValidationError as exc: + self.context['error'] = True + self.context['msg'] = exc.message + + if self.context['error']: + return self.render_response() + + source_store = modulestore()._get_modulestore_for_courselike(course_usage_key) # pylint: disable=protected-access + if not hasattr(source_store, 'force_publish_course'): + self.context['msg'] = _('Force publishing course is not supported with old mongo courses.') + log.warning( + 'Force publishing course is not supported with old mongo courses. \ + %s attempted to force publish the course %s.', + request.user, + course_id, + exc_info=True + ) + return self.render_response() + + current_versions = self.get_course_branch_versions(get_course_versions(course_id)) + + # if publish and draft are NOT different + if current_versions['published-branch'] == current_versions['draft-branch']: + self.context['msg'] = _('Course is already in published state.') + log.warning( + 'Course is already in published state. %s attempted to force publish the course %s.', + request.user, + course_id, + exc_info=True + ) + return self.render_response() + + self.context['current_versions'] = current_versions + log.info( + '%s dry ran force publish the course %s.', + request.user, + course_id, + exc_info=True + ) + return self.render_response() diff --git a/cms/envs/common.py b/cms/envs/common.py index 09b8f2648b..361ae01d78 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -834,6 +834,9 @@ INSTALLED_APPS = ( 'openedx.core.djangoapps.coursetalk', # not used in cms (yet), but tests run 'xblock_config', + # Maintenance tools + 'maintenance', + # Tracking 'track', 'eventtracking.django.apps.EventTrackingConfig', @@ -933,6 +936,9 @@ INSTALLED_APPS = ( # Enables default site and redirects 'django_sites_extensions', + + # additional release utilities to ease automation + 'release_util' ) diff --git a/cms/static/cms/js/build.js b/cms/static/cms/js/build.js index ca5f98eaf7..fd90513c2a 100644 --- a/cms/static/cms/js/build.js +++ b/cms/static/cms/js/build.js @@ -16,7 +16,7 @@ var getModulesList = function(modules) { var result = [getModule(commonLibrariesPath)]; - return result.concat(modules.map(function (moduleName) { + return result.concat(modules.map(function(moduleName) { return getModule(moduleName, true); })); }; @@ -171,4 +171,5 @@ */ logLevel: 1 }; -}()) +}()) // eslint-disable-line semi +// A semicolon on the line above will break the requirejs optimizer diff --git a/cms/static/cms/js/require-config.js b/cms/static/cms/js/require-config.js index ebce5dc0f1..45aadf6498 100644 --- a/cms/static/cms/js/require-config.js +++ b/cms/static/cms/js/require-config.js @@ -1,4 +1,4 @@ -;(function(require, define) { +(function(require, define) { 'use strict'; if (window) { @@ -210,12 +210,12 @@ window.MathJax.Hub.Config({ tex2jax: { inlineMath: [ - ['\\(','\\)'], - ['[mathjaxinline]','[/mathjaxinline]'] + ['\\(', '\\)'], + ['[mathjaxinline]', '[/mathjaxinline]'] ], displayMath: [ - ['\\[','\\]'], - ['[mathjax]','[/mathjax]'] + ['\\[', '\\]'], + ['[mathjax]', '[/mathjax]'] ] } }); diff --git a/cms/static/cms/js/spec/main_spec.js b/cms/static/cms/js/spec/main_spec.js index e61d25d444..ed01a312b2 100644 --- a/cms/static/cms/js/spec/main_spec.js +++ b/cms/static/cms/js/spec/main_spec.js @@ -2,81 +2,81 @@ (function(sandbox) { 'use strict'; - require(["jquery", "backbone", "cms/js/main", "edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers", "jquery.cookie"], + require(['jquery', 'backbone', 'cms/js/main', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'jquery.cookie'], function($, Backbone, main, AjaxHelpers) { - describe("CMS", function() { - it("should initialize URL", function() { - expect(window.CMS.URL).toBeDefined(); - }); - }); - describe("main helper", function() { - beforeEach(function() { - this.previousAjaxSettings = $.extend(true, {}, $.ajaxSettings); - spyOn($, "cookie").and.callFake(function(param) { - if (param === "csrftoken") { - return "stubCSRFToken"; - } + describe('CMS', function() { + it('should initialize URL', function() { + expect(window.CMS.URL).toBeDefined(); + }); }); - return main(); - }); - afterEach(function() { - $.ajaxSettings = this.previousAjaxSettings; - return $.ajaxSettings; - }); - it("turn on Backbone emulateHTTP", function() { - expect(Backbone.emulateHTTP).toBeTruthy(); - }); - it("setup AJAX CSRF token", function() { - expect($.ajaxSettings.headers["X-CSRFToken"]).toEqual("stubCSRFToken"); - }); - }); - describe("AJAX Errors", function() { - var server; - server = null; - beforeEach(function() { - appendSetFixtures(sandbox({ - id: "page-notification" - })); - }); - afterEach(function() { - return server && server.restore(); - }); - it("successful AJAX request does not pop an error notification", function() { - server = AjaxHelpers.server([ - 200, { - "Content-Type": "application/json" - }, "{}" - ]); - expect($("#page-notification")).toBeEmpty(); - $.ajax("/test"); - expect($("#page-notification")).toBeEmpty(); - server.respond(); - expect($("#page-notification")).toBeEmpty(); - }); - it("AJAX request with error should pop an error notification", function() { - server = AjaxHelpers.server([ - 500, { - "Content-Type": "application/json" - }, "{}" - ]); - $.ajax("/test"); - server.respond(); - expect($("#page-notification")).not.toBeEmpty(); - expect($("#page-notification")).toContainElement('div.wrapper-notification-error'); - }); - it("can override AJAX request with error so it does not pop an error notification", function() { - server = AjaxHelpers.server([ - 500, { - "Content-Type": "application/json" - }, "{}" - ]); - $.ajax({ - url: "/test", - notifyOnError: false + describe('main helper', function() { + beforeEach(function() { + this.previousAjaxSettings = $.extend(true, {}, $.ajaxSettings); + spyOn($, 'cookie').and.callFake(function(param) { + if (param === 'csrftoken') { + return 'stubCSRFToken'; + } + }); + return main(); + }); + afterEach(function() { + $.ajaxSettings = this.previousAjaxSettings; + return $.ajaxSettings; + }); + it('turn on Backbone emulateHTTP', function() { + expect(Backbone.emulateHTTP).toBeTruthy(); + }); + it('setup AJAX CSRF token', function() { + expect($.ajaxSettings.headers['X-CSRFToken']).toEqual('stubCSRFToken'); + }); + }); + describe('AJAX Errors', function() { + var server; + server = null; + beforeEach(function() { + appendSetFixtures(sandbox({ + id: 'page-notification' + })); + }); + afterEach(function() { + return server && server.restore(); + }); + it('successful AJAX request does not pop an error notification', function() { + server = AjaxHelpers.server([ + 200, { + 'Content-Type': 'application/json' + }, '{}' + ]); + expect($('#page-notification')).toBeEmpty(); + $.ajax('/test'); + expect($('#page-notification')).toBeEmpty(); + server.respond(); + expect($('#page-notification')).toBeEmpty(); + }); + it('AJAX request with error should pop an error notification', function() { + server = AjaxHelpers.server([ + 500, { + 'Content-Type': 'application/json' + }, '{}' + ]); + $.ajax('/test'); + server.respond(); + expect($('#page-notification')).not.toBeEmpty(); + expect($('#page-notification')).toContainElement('div.wrapper-notification-error'); + }); + it('can override AJAX request with error so it does not pop an error notification', function() { + server = AjaxHelpers.server([ + 500, { + 'Content-Type': 'application/json' + }, '{}' + ]); + $.ajax({ + url: '/test', + notifyOnError: false + }); + server.respond(); + expect($('#page-notification')).toBeEmpty(); + }); }); - server.respond(); - expect($("#page-notification")).toBeEmpty(); }); - }); - }); }).call(this, sandbox); diff --git a/cms/static/cms/js/xblock/cms.runtime.v1.js b/cms/static/cms/js/xblock/cms.runtime.v1.js index 6c5e73aaf3..6b07ec80e1 100644 --- a/cms/static/cms/js/xblock/cms.runtime.v1.js +++ b/cms/static/cms/js/xblock/cms.runtime.v1.js @@ -24,7 +24,6 @@ define(['jquery', 'backbone', 'xblock/runtime.v1', 'URI', 'gettext', 'js/utils/m StudioRuntime = {}; BaseRuntime.v1 = (function(_super) { - __extends(v1, _super); v1.prototype.handlerUrl = function(element, handlerName, suffix, query) { @@ -150,11 +149,9 @@ define(['jquery', 'backbone', 'xblock/runtime.v1', 'URI', 'gettext', 'js/utils/m }; return v1; - })(XBlock.Runtime.v1); PreviewRuntime.v1 = (function(_super) { - __extends(v1, _super); function v1() { @@ -164,11 +161,9 @@ define(['jquery', 'backbone', 'xblock/runtime.v1', 'URI', 'gettext', 'js/utils/m v1.prototype.handlerPrefix = '/preview/xblock'; return v1; - })(BaseRuntime.v1); StudioRuntime.v1 = (function(_super) { - __extends(v1, _super); function v1() { @@ -178,7 +173,6 @@ define(['jquery', 'backbone', 'xblock/runtime.v1', 'URI', 'gettext', 'js/utils/m v1.prototype.handlerPrefix = '/xblock'; return v1; - })(BaseRuntime.v1); // Install the runtime's into the global namespace diff --git a/cms/static/js/base.js b/cms/static/js/base.js index 94c6cc2648..ed2c75f7bf 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -1,19 +1,19 @@ require([ - "domReady", - "jquery", - "underscore", - "gettext", - "common/js/components/views/feedback_notification", - "common/js/components/views/feedback_prompt", - "js/utils/date_utils", - "js/utils/module", - "js/utils/handle_iframe_binding", - "edx-ui-toolkit/js/dropdown-menu/dropdown-menu-view", - "jquery.ui", - "jquery.leanModal", - "jquery.form", - "jquery.smoothScroll" - ], + 'domReady', + 'jquery', + 'underscore', + 'gettext', + 'common/js/components/views/feedback_notification', + 'common/js/components/views/feedback_prompt', + 'js/utils/date_utils', + 'js/utils/module', + 'js/utils/handle_iframe_binding', + 'edx-ui-toolkit/js/dropdown-menu/dropdown-menu-view', + 'jquery.ui', + 'jquery.leanModal', + 'jquery.form', + 'jquery.smoothScroll' +], function( domReady, $, @@ -27,113 +27,110 @@ require([ DropdownMenuView ) { + var $body; -var $body; + domReady(function() { + var dropdownMenuView; -domReady(function() { - var dropdownMenuView; - - $body = $('body'); + $body = $('body'); - $body.on('click', '.embeddable-xml-input', function() { - $(this).select(); - }); + $body.on('click', '.embeddable-xml-input', function() { + $(this).select(); + }); - $body.addClass('js'); + $body.addClass('js'); - // alerts/notifications - manual close - $('.action-alert-close, .alert.has-actions .nav-actions a').bind('click', hideAlert); - $('.action-notification-close').bind('click', hideNotification); + // alerts/notifications - manual close + $('.action-alert-close, .alert.has-actions .nav-actions a').bind('click', hideAlert); + $('.action-notification-close').bind('click', hideNotification); - // nav - dropdown related - $body.click(function(e) { - $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); - $('.nav-dd .nav-item .title').removeClass('is-selected'); - }); + // nav - dropdown related + $body.click(function(e) { + $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); + $('.nav-dd .nav-item .title').removeClass('is-selected'); + }); - $('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) { + $('.nav-dd .nav-item, .filterable-column .nav-item').click(function(e) { + $subnav = $(this).find('.wrapper-nav-sub'); + $title = $(this).find('.title'); - $subnav = $(this).find('.wrapper-nav-sub'); - $title = $(this).find('.title'); - - if ($subnav.hasClass('is-shown')) { - $subnav.removeClass('is-shown'); - $title.removeClass('is-selected'); - } else { - $('.nav-dd .nav-item .title').removeClass('is-selected'); - $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); - $title.addClass('is-selected'); - $subnav.addClass('is-shown'); + if ($subnav.hasClass('is-shown')) { + $subnav.removeClass('is-shown'); + $title.removeClass('is-selected'); + } else { + $('.nav-dd .nav-item .title').removeClass('is-selected'); + $('.nav-dd .nav-item .wrapper-nav-sub').removeClass('is-shown'); + $title.addClass('is-selected'); + $subnav.addClass('is-shown'); // if propagation is not stopped, the event will bubble up to the // body element, which will close the dropdown. - e.stopPropagation(); - } - }); + e.stopPropagation(); + } + }); - // general link management - new window/tab - $('a[rel="external"]:not([title])').attr('title', gettext('This link will open in a new browser window/tab')); - $('a[rel="external"]').attr('target', '_blank'); + // general link management - new window/tab + $('a[rel="external"]:not([title])').attr('title', gettext('This link will open in a new browser window/tab')); + $('a[rel="external"]').attr('target', '_blank'); - // general link management - lean modal window - $('a[rel="modal"]').attr('title', gettext('This link will open in a modal window')).leanModal({ - overlay: 0.50, - closeButton: '.action-modal-close' - }); - $('.action-modal-close').click(function(e) { - (e).preventDefault(); - }); + // general link management - lean modal window + $('a[rel="modal"]').attr('title', gettext('This link will open in a modal window')).leanModal({ + overlay: 0.50, + closeButton: '.action-modal-close' + }); + $('.action-modal-close').click(function(e) { + (e).preventDefault(); + }); - // general link management - smooth scrolling page links - $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); + // general link management - smooth scrolling page links + $('a[rel*="view"][href^="#"]').bind('click', smoothScrollLink); - IframeUtils.iframeBinding(); + IframeUtils.iframeBinding(); - // disable ajax caching in IE so that backbone fetches work - if ($.browser.msie) { - $.ajaxSetup({ cache: false }); - } + // disable ajax caching in IE so that backbone fetches work + if ($.browser.msie) { + $.ajaxSetup({cache: false}); + } - //Initiate the edx tool kit dropdown menu - if ($('.js-header-user-menu').length){ - dropdownMenuView = new DropdownMenuView({ - el: '.js-header-user-menu' + // Initiate the edx tool kit dropdown menu + if ($('.js-header-user-menu').length) { + dropdownMenuView = new DropdownMenuView({ + el: '.js-header-user-menu' + }); + dropdownMenuView.postRender(); + } }); - dropdownMenuView.postRender(); - } -}); -function smoothScrollLink(e) { - (e).preventDefault(); + function smoothScrollLink(e) { + (e).preventDefault(); - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $(this).attr('href') - }); -} + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $(this).attr('href') + }); + } -function smoothScrollTop(e) { - (e).preventDefault(); + function smoothScrollTop(e) { + (e).preventDefault(); - $.smoothScroll({ - offset: -200, - easing: 'swing', - speed: 1000, - scrollElement: null, - scrollTarget: $('#view-top') - }); -} + $.smoothScroll({ + offset: -200, + easing: 'swing', + speed: 1000, + scrollElement: null, + scrollTarget: $('#view-top') + }); + } -function hideNotification(e) { - (e).preventDefault(); - $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden', 'true'); -} + function hideNotification(e) { + (e).preventDefault(); + $(this).closest('.wrapper-notification').removeClass('is-shown').addClass('is-hiding').attr('aria-hidden', 'true'); + } -function hideAlert(e) { - (e).preventDefault(); - $(this).closest('.wrapper-alert').removeClass('is-shown'); -} - -}); // end require() + function hideAlert(e) { + (e).preventDefault(); + $(this).closest('.wrapper-alert').removeClass('is-shown'); + } + }); // end require() diff --git a/cms/static/js/certificates/collections/certificates.js b/cms/static/js/certificates/collections/certificates.js index 778a43f7ed..e567514d21 100644 --- a/cms/static/js/certificates/collections/certificates.js +++ b/cms/static/js/certificates/collections/certificates.js @@ -29,7 +29,7 @@ function(Backbone, gettext, Certificate) { } catch (ex) { // If it didn't parse, and `certificate_info` is an object then return as it is // otherwise return empty array - if (typeof certificate_info === 'object'){ + if (typeof certificate_info === 'object') { return_array = certificate_info; } else { @@ -44,28 +44,28 @@ function(Backbone, gettext, Certificate) { return return_array; }, - onModelRemoved: function () { + onModelRemoved: function() { // remove the certificate web preview UI. - if(window.certWebPreview && this.length === 0) { + if (window.certWebPreview && this.length === 0) { window.certWebPreview.remove(); } this.toggleAddNewItemButtonState(); }, - onModelAdd: function () { + onModelAdd: function() { this.toggleAddNewItemButtonState(); }, toggleAddNewItemButtonState: function() { // user can create a new item e.g certificate; if not exceeded the maxAllowed limit. - if(this.length >= this.maxAllowed) { - $(".action-add").addClass('action-add-hidden'); + if (this.length >= this.maxAllowed) { + $('.action-add').addClass('action-add-hidden'); } else { - $(".action-add").removeClass('action-add-hidden'); + $('.action-add').removeClass('action-add-hidden'); } }, - parse: function (certificatesJson) { + parse: function(certificatesJson) { // Transforms the provided JSON into a Certificates collection var modelArray = this.certificate_array(certificatesJson); diff --git a/cms/static/js/certificates/factories/certificates_page_factory.js b/cms/static/js/certificates/factories/certificates_page_factory.js index 39d7941f42..cb935ddb06 100644 --- a/cms/static/js/certificates/factories/certificates_page_factory.js +++ b/cms/static/js/certificates/factories/certificates_page_factory.js @@ -20,7 +20,7 @@ define([ ], function($, CertificatesCollection, Certificate, CertificatesPage, CertificatePreview) { 'use strict'; - return function (certificatesJson, certificateUrl, courseOutlineUrl, course_modes, certificate_web_view_url, + return function(certificatesJson, certificateUrl, courseOutlineUrl, course_modes, certificate_web_view_url, is_active, certificate_activation_handler_url) { // Initialize the model collection, passing any necessary options to the constructor var certificatesCollection = new CertificatesCollection(certificatesJson, { @@ -31,7 +31,7 @@ function($, CertificatesCollection, Certificate, CertificatesPage, CertificatePr // associating the certificate_preview globally. // need to show / hide this view in some other places. - if(!window.certWebPreview && certificate_web_view_url) { + if (!window.certWebPreview && certificate_web_view_url) { window.certWebPreview = new CertificatePreview({ course_modes: course_modes, certificate_web_view_url: certificate_web_view_url, diff --git a/cms/static/js/certificates/models/certificate.js b/cms/static/js/certificates/models/certificate.js index 5518867312..a440d569d6 100644 --- a/cms/static/js/certificates/models/certificate.js +++ b/cms/static/js/certificates/models/certificate.js @@ -1,15 +1,15 @@ // Backbone.js Application Model: Certificate define([ - 'underscore', - 'backbone', - 'backbone-relational', - 'backbone.associations', - 'gettext', - 'cms/js/main', - 'js/certificates/models/signatory', - 'js/certificates/collections/signatories' - ], + 'underscore', + 'backbone', + 'backbone-relational', + 'backbone.associations', + 'gettext', + 'cms/js/main', + 'js/certificates/models/signatory', + 'js/certificates/collections/signatories' +], function(_, Backbone, BackboneRelational, BackboneAssociations, gettext, CoffeeSrcMain, SignatoryModel, SignatoryCollection) { 'use strict'; @@ -78,7 +78,7 @@ define([ attributes: {name: true} }; } - var allSignatoriesValid = _.every(attrs.signatories.models, function(signatory){ + var allSignatoriesValid = _.every(attrs.signatories.models, function(signatory) { return signatory.isValid(); }); if (!allSignatoriesValid) { diff --git a/cms/static/js/certificates/spec/custom_matchers.js b/cms/static/js/certificates/spec/custom_matchers.js index 38e21459e0..cd960f469f 100644 --- a/cms/static/js/certificates/spec/custom_matchers.js +++ b/cms/static/js/certificates/spec/custom_matchers.js @@ -3,13 +3,13 @@ define(['jquery'], function($) { // eslint-disable-line no-unused-vars 'use strict'; - return function () { + return function() { jasmine.addMatchers({ - toBeCorrectValuesInModel: function () { + toBeCorrectValuesInModel: function() { // Assert the value being tested has key values which match the provided values return { - compare: function (actual, values) { - var passed = _.every(values, function (value, key) { + compare: function(actual, values) { + var passed = _.every(values, function(value, key) { return actual.get(key) === value; }.bind(this)); diff --git a/cms/static/js/certificates/spec/models/certificate_spec.js b/cms/static/js/certificates/spec/models/certificate_spec.js index bfd229feab..6c37488859 100644 --- a/cms/static/js/certificates/spec/models/certificate_spec.js +++ b/cms/static/js/certificates/spec/models/certificate_spec.js @@ -11,7 +11,7 @@ function(CertificateModel, CertificateCollection) { beforeEach(function() { this.newModelOptions = {add: true}; this.model = new CertificateModel({editing: true}, this.newModelOptions); - this.collection = new CertificateCollection([ this.model ], {certificateUrl: '/outline'}); + this.collection = new CertificateCollection([this.model], {certificateUrl: '/outline'}); }); describe('Basic', function() { @@ -39,18 +39,16 @@ function(CertificateModel, CertificateCollection) { describe('Validation', function() { it('requires a name', function() { - var model = new CertificateModel({ name: '' }, this.newModelOptions); + var model = new CertificateModel({name: ''}, this.newModelOptions); expect(model.isValid()).toBeFalsy(); }); it('can pass validation', function() { - var model = new CertificateModel({ name: 'foo' }, this.newModelOptions); + var model = new CertificateModel({name: 'foo'}, this.newModelOptions); expect(model.isValid()).toBeTruthy(); }); - }); }); - }); diff --git a/cms/static/js/certificates/spec/views/certificate_details_spec.js b/cms/static/js/certificates/spec/views/certificate_details_spec.js index 863fac166f..4a58c5d97e 100644 --- a/cms/static/js/certificates/spec/views/certificate_details_spec.js +++ b/cms/static/js/certificates/spec/views/certificate_details_spec.js @@ -41,15 +41,15 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails inputSignatoryTitle: '.signatory-title-input', inputSignatoryOrganization: '.signatory-organization-input' }; - var verifyAndConfirmPrompt = function(promptSpy, promptText){ + var verifyAndConfirmPrompt = function(promptSpy, promptText) { ViewHelpers.verifyPromptShowing(promptSpy, gettext(promptText)); ViewHelpers.confirmPrompt(promptSpy); ViewHelpers.verifyPromptHidden(promptSpy); }; describe('Certificate Details Spec:', function() { - var setValuesToInputs = function (view, values) { - _.each(values, function (value, selector) { + var setValuesToInputs = function(view, values) { + _.each(values, function(value, selector) { if (SELECTORS[selector]) { view.$(SELECTORS[selector]).val(value); view.$(SELECTORS[selector]).trigger('change'); @@ -96,8 +96,8 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails is_active: true }, this.newModelOptions); - this.collection = new CertificatesCollection([ this.model ], { - certificateUrl: '/certificates/'+ window.course.id + this.collection = new CertificatesCollection([this.model], { + certificateUrl: '/certificates/' + window.course.id }); this.model.set('id', 0); this.view = new CertificateDetailsView({ @@ -120,44 +120,43 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails describe('The Certificate Details view', function() { - - it('should parse a JSON string collection into a Backbone model collection', function () { - var course_title = "Test certificate course title override 2"; + it('should parse a JSON string collection into a Backbone model collection', function() { + var course_title = 'Test certificate course title override 2'; var CERTIFICATE_JSON = '[{"course_title": "' + course_title + '", "signatories":"[]"}]'; this.collection.parse(CERTIFICATE_JSON); var model = this.collection.at(1); expect(model.get('course_title')).toEqual(course_title); }); - it('should parse a JSON object collection into a Backbone model collection', function () { - var course_title = "Test certificate course title override 2"; + it('should parse a JSON object collection into a Backbone model collection', function() { + var course_title = 'Test certificate course title override 2'; var CERTIFICATE_JSON_OBJECT = [{ - "course_title" : course_title, - "signatories" : "[]" + 'course_title': course_title, + 'signatories': '[]' }]; this.collection.parse(CERTIFICATE_JSON_OBJECT); var model = this.collection.at(1); expect(model.get('course_title')).toEqual(course_title); }); - it('should have empty certificate collection if there is an error parsing certifcate JSON', function () { + it('should have empty certificate collection if there is an error parsing certifcate JSON', function() { var CERTIFICATE_INVALID_JSON = '[{"course_title": Test certificate course title override, "signatories":"[]"}]'; // eslint-disable-line max-len var collection_length = this.collection.length; this.collection.parse(CERTIFICATE_INVALID_JSON); - //collection length should remain the same since we have error parsing JSON + // collection length should remain the same since we have error parsing JSON expect(this.collection.length).toEqual(collection_length); }); - it('should display the certificate course title override', function () { + it('should display the certificate course title override', function() { expect(this.view.$(SELECTORS.course_title)).toExist(); expect(this.view.$(SELECTORS.course_title)).toContainText('Test Course Title Override'); }); - it('should present an Edit action', function () { + it('should present an Edit action', function() { expect(this.view.$('.edit')).toExist(); }); - it('should change to "edit" mode when clicking the Edit button and confirming the prompt', function(){ + it('should change to "edit" mode when clicking the Edit button and confirming the prompt', function() { expect(this.view.$('.action-edit .edit')).toExist(); var promptSpy = ViewHelpers.createPromptSpy(); this.view.$('.action-edit .edit').click(); @@ -165,67 +164,64 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails expect(this.model.get('editing')).toBe(true); }); - it('should not show confirmation prompt when clicked on "edit" in case of inactive certificate', function(){ + it('should not show confirmation prompt when clicked on "edit" in case of inactive certificate', function() { this.model.set('is_active', false); expect(this.view.$('.action-edit .edit')).toExist(); this.view.$('.action-edit .edit').click(); expect(this.model.get('editing')).toBe(true); }); - it('should not present a Edit action if user is not global staff and certificate is active', function () { + it('should not present a Edit action if user is not global staff and certificate is active', function() { window.CMS.User = {isGlobalStaff: false}; appendSetFixtures(this.view.render().el); expect(this.view.$('.action-edit .edit')).not.toExist(); }); - it('should present a Delete action', function () { + it('should present a Delete action', function() { expect(this.view.$('.action-delete .delete')).toExist(); }); - it('should not present a Delete action if user is not global staff and certificate is active', function () { + it('should not present a Delete action if user is not global staff and certificate is active', function() { window.CMS.User = {isGlobalStaff: false}; appendSetFixtures(this.view.render().el); expect(this.view.$('.action-delete .delete')).not.toExist(); }); - it('should prompt the user when when clicking the Delete button', function(){ + it('should prompt the user when when clicking the Delete button', function() { expect(this.view.$('.action-delete .delete')).toExist(); this.view.$('.action-delete .delete').click(); }); - it('should scroll to top after rendering if necessary', function () { + it('should scroll to top after rendering if necessary', function() { $.smoothScroll = jasmine.createSpy('jQuery.smoothScroll'); appendSetFixtures(this.view.render().el); expect($.smoothScroll).toHaveBeenCalled(); }); - }); - describe('Signatory details', function(){ - + describe('Signatory details', function() { beforeEach(function() { this.view.render(); }); - it('displays certificate signatories details', function(){ + it('displays certificate signatories details', function() { this.view.$('.show-details').click(); expect(this.view.$(SELECTORS.signatory_name_value)).toContainText(''); expect(this.view.$(SELECTORS.signatory_title_value)).toContainText(''); expect(this.view.$(SELECTORS.signatory_organization_value)).toContainText(''); }); - it('should present Edit action on signaotry', function () { + it('should present Edit action on signaotry', function() { expect(this.view.$(SELECTORS.edit_signatory)).toExist(); }); - it('should not present Edit action on signaotry if user is not global staff and certificate is active', function () { + it('should not present Edit action on signaotry if user is not global staff and certificate is active', function() { window.CMS.User = {isGlobalStaff: false}; this.view.render(); expect(this.view.$(SELECTORS.edit_signatory)).not.toExist(); }); it('supports in-line editing of signatory information', function() { - this.view.$(SELECTORS.edit_signatory).click(); expect(this.view.$(SELECTORS.inputSignatoryName)).toExist(); expect(this.view.$(SELECTORS.inputSignatoryTitle)).toExist(); @@ -233,7 +229,6 @@ function(_, Course, CertificatesCollection, CertificateModel, CertificateDetails }); it('correctly persists changes made during in-line signatory editing', function() { - var requests = AjaxHelpers.requests(this), notificationSpy = ViewHelpers.createNotificationSpy(); diff --git a/cms/static/js/certificates/spec/views/certificate_editor_spec.js b/cms/static/js/certificates/spec/views/certificate_editor_spec.js index f674a9d56b..39416e56a8 100644 --- a/cms/static/js/certificates/spec/views/certificate_editor_spec.js +++ b/cms/static/js/certificates/spec/views/certificate_editor_spec.js @@ -37,14 +37,14 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce note: '.wrapper-delete-button', addSignatoryButton: '.action-add-signatory', signatoryDeleteButton: '.signatory-panel-delete', - uploadSignatureButton:'.action-upload-signature', + uploadSignatureButton: '.action-upload-signature', uploadDialog: 'form.upload-dialog', uploadDialogButton: '.action-upload', uploadDialogFileInput: 'form.upload-dialog input[type=file]', saveCertificateButton: 'button.action-primary' }; - var clickDeleteItem = function (that, promptText, element, url) { + var clickDeleteItem = function(that, promptText, element, url) { var requests = AjaxHelpers.requests(that), promptSpy = ViewHelpers.createPromptSpy(), notificationSpy = ViewHelpers.createNotificationSpy(); @@ -53,7 +53,7 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce ViewHelpers.verifyPromptShowing(promptSpy, promptText); ViewHelpers.confirmPrompt(promptSpy); ViewHelpers.verifyPromptHidden(promptSpy); - if (!_.isUndefined(url) && !_.isEmpty(url)){ + if (!_.isUndefined(url) && !_.isEmpty(url)) { AjaxHelpers.expectJsonRequest(requests, 'POST', url); expect(_.last(requests).requestHeaders['X-HTTP-Method-Override']).toBe('DELETE'); ViewHelpers.verifyNotificationShowing(notificationSpy, /Deleting/); @@ -62,7 +62,7 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce } }; - var showConfirmPromptAndClickCancel = function (view, element, promptText) { + var showConfirmPromptAndClickCancel = function(view, element, promptText) { var promptSpy = ViewHelpers.createPromptSpy(); view.$(element).click(); ViewHelpers.verifyPromptShowing(promptSpy, promptText); @@ -70,15 +70,15 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce ViewHelpers.verifyPromptHidden(promptSpy); }; - var uploadFile = function (file_path, requests){ + var uploadFile = function(file_path, requests) { $(SELECTORS.uploadDialogFileInput).change(); $(SELECTORS.uploadDialogButton).click(); AjaxHelpers.respondWithJson(requests, {asset: {url: file_path}}); }; describe('Certificate editor view', function() { - var setValuesToInputs = function (view, values) { - _.each(values, function (value, selector) { + var setValuesToInputs = function(view, values) { + _.each(values, function(value, selector) { if (SELECTORS[selector]) { view.$(SELECTORS[selector]).val(value); view.$(SELECTORS[selector]).trigger('change'); @@ -86,8 +86,8 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce }); }; var basicModalTpl = readFixtures('basic-modal.underscore'), - modalButtonTpl = readFixtures('modal-button.underscore'), - uploadDialogTpl = readFixtures('upload-dialog.underscore'); + modalButtonTpl = readFixtures('modal-button.underscore'), + uploadDialogTpl = readFixtures('upload-dialog.underscore'); beforeEach(function() { TemplateHelpers.installTemplates(['certificate-editor', 'signatory-editor'], true); @@ -110,8 +110,8 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce }, this.newModelOptions); - this.collection = new CertificatesCollection([ this.model ], { - certificateUrl: '/certificates/'+ window.course.id + this.collection = new CertificatesCollection([this.model], { + certificateUrl: '/certificates/' + window.course.id }); this.model.set('id', 0); this.view = new CertificateEditorView({ @@ -127,20 +127,20 @@ function(_, Course, CertificateModel, SignatoryModel, CertificatesCollection, Ce delete window.CMS.User; }); - describe('Basic', function () { - beforeEach(function(){ + describe('Basic', function() { + beforeEach(function() { appendSetFixtures( - $(""); - } else if (kind === "url") { + } else if (mimetype === 'application/javascript') { + if (kind === 'text') { + head.append(''); + } else if (kind === 'url') { return ViewUtils.loadJavaScript(data); } - } else if (mimetype === "text/html") { - if (placement === "head") { + } else if (mimetype === 'text/html') { + if (placement === 'head') { head.append(data); } } @@ -224,10 +224,10 @@ define(["jquery", "underscore", "common/js/components/utils/view_utils", "js/vie }, fireNotificationActionEvent: function(event) { - var eventName = $(event.currentTarget).data("notification-action"); + var eventName = $(event.currentTarget).data('notification-action'); if (eventName) { event.preventDefault(); - this.notifyRuntime(eventName, this.model.get("id")); + this.notifyRuntime(eventName, this.model.get('id')); } } }); diff --git a/cms/static/js/views/xblock_editor.js b/cms/static/js/views/xblock_editor.js index e549fa6b54..e2a52672ea 100644 --- a/cms/static/js/views/xblock_editor.js +++ b/cms/static/js/views/xblock_editor.js @@ -2,10 +2,9 @@ * XBlockEditorView displays the authoring view of an xblock, and allows the user to switch between * the available modes. */ -define(["jquery", "underscore", "gettext", "js/views/xblock", "js/views/metadata", "js/collections/metadata", - "jquery.inputnumber"], - function ($, _, gettext, XBlockView, MetadataView, MetadataCollection) { - +define(['jquery', 'underscore', 'gettext', 'js/views/xblock', 'js/views/metadata', 'js/collections/metadata', + 'jquery.inputnumber'], + function($, _, gettext, XBlockView, MetadataView, MetadataCollection) { var XBlockEditorView = XBlockView.extend({ // takes XBlockInfo as a model @@ -40,8 +39,8 @@ define(["jquery", "underscore", "gettext", "js/views/xblock", "js/views/metadata getDefaultModes: function() { return [ - { id: 'editor', name: gettext("Editor")}, - { id: 'settings', name: gettext("Settings")} + {id: 'editor', name: gettext('Editor')}, + {id: 'settings', name: gettext('Settings')} ]; }, @@ -134,7 +133,7 @@ define(["jquery", "underscore", "gettext", "js/views/xblock", "js/views/metadata metadataNameElements = this.$('[data-metadata-name]'); for (i = 0; i < metadataNameElements.length; i++) { element = metadataNameElements[i]; - metadataName = $(element).data("metadata-name"); + metadataName = $(element).data('metadata-name'); metadata[metadataName] = element.value; } return metadata; diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js index 2d4e04398d..6e58b1c67d 100644 --- a/cms/static/js/views/xblock_outline.js +++ b/cms/static/js/views/xblock_outline.js @@ -13,10 +13,9 @@ * - scroll_offset - the scroll offset to use for the locator being shown * - edit_display_name - true if the shown xblock's display name should be in inline edit mode */ -define(["jquery", "underscore", "gettext", "js/views/baseview", "common/js/components/utils/view_utils", - "js/views/utils/xblock_utils", "js/views/xblock_string_field_editor"], +define(['jquery', 'underscore', 'gettext', 'js/views/baseview', 'common/js/components/utils/view_utils', + 'js/views/utils/xblock_utils', 'js/views/xblock_string_field_editor'], function($, _, gettext, BaseView, ViewUtils, XBlockViewUtils, XBlockStringFieldEditor) { - var XBlockOutlineView = BaseView.extend({ // takes XBlockInfo as a model @@ -49,7 +48,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "common/js/compo // need to add the current model's id/locator to the set of expanded locators if (this.model.get('is_header_visible') !== null && !this.model.get('is_header_visible')) { var locator = this.model.get('id'); - if(!_.isUndefined(this.expandedLocators) && !this.expandedLocators.contains(locator)) { + if (!_.isUndefined(this.expandedLocators) && !this.expandedLocators.contains(locator)) { this.expandedLocators.add(locator); this.refresh(); } @@ -216,7 +215,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "common/js/compo onSync: function(event) { if (ViewUtils.hasChangedAttributes(this.model, ['visibility_state', 'child_info', 'display_name'])) { - this.onXBlockChange(); + this.onXBlockChange(); } }, @@ -246,7 +245,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "common/js/compo if (locatorElement.length > 0) { ViewUtils.setScrollOffset(locatorElement, scrollOffset); } else { - console.error("Failed to show item with locator " + locatorToShow + ""); + console.error('Failed to show item with locator ' + locatorToShow + ''); } if (editDisplayName) { locatorElement.find('> div[class$="header"] .xblock-field-value-edit').click(); diff --git a/cms/static/js/views/xblock_string_field_editor.js b/cms/static/js/views/xblock_string_field_editor.js index ad9de8fdaa..d9eeb14fce 100644 --- a/cms/static/js/views/xblock_string_field_editor.js +++ b/cms/static/js/views/xblock_string_field_editor.js @@ -5,9 +5,8 @@ * XBlock field's value if it has been changed. If the user presses Escape, then any changes will * be removed and the input hidden again. */ -define(["js/views/baseview", "js/views/utils/xblock_utils"], - function (BaseView, XBlockViewUtils) { - +define(['js/views/baseview', 'js/views/utils/xblock_utils'], + function(BaseView, XBlockViewUtils) { var XBlockStringFieldEditor = BaseView.extend({ events: { 'click .xblock-field-value-edit': 'showInput', @@ -42,7 +41,7 @@ define(["js/views/baseview", "js/views/utils/xblock_utils"], return this.$('.xblock-field-value'); }, - getInput: function () { + getInput: function() { return this.$('.xblock-field-input'); }, diff --git a/cms/static/js/views/xblock_validation.js b/cms/static/js/views/xblock_validation.js index 4733c30f9d..a0e6bcf010 100644 --- a/cms/static/js/views/xblock_validation.js +++ b/cms/static/js/views/xblock_validation.js @@ -1,5 +1,5 @@ -define(["jquery", "underscore", "js/views/baseview", "gettext"], - function ($, _, BaseView, gettext) { +define(['jquery', 'underscore', 'js/views/baseview', 'gettext'], + function($, _, BaseView, gettext) { /** * View for xblock validation messages as displayed in Studio. */ @@ -12,7 +12,7 @@ define(["jquery", "underscore", "js/views/baseview", "gettext"], this.root = options.root; }, - render: function () { + render: function() { this.$el.html(this.template({ validation: this.model, additionalClasses: this.getAdditionalClasses(), @@ -27,7 +27,7 @@ define(["jquery", "underscore", "js/views/baseview", "gettext"], * @param messageType * @returns string representation of css class that will render the correct icon, or null if unknown type */ - getIcon: function (messageType) { + getIcon: function(messageType) { if (messageType === this.model.ERROR) { return 'fa-exclamation-circle'; } @@ -42,16 +42,16 @@ define(["jquery", "underscore", "js/views/baseview", "gettext"], * @param messageType * @returns string display name (translated) */ - getDisplayName: function (messageType) { + getDisplayName: function(messageType) { if (messageType === this.model.WARNING || messageType === this.model.NOT_CONFIGURED) { // Translators: This message will be added to the front of messages of type warning, // e.g. "Warning: this component has not been configured yet". - return gettext("Warning"); + return gettext('Warning'); } else if (messageType === this.model.ERROR) { // Translators: This message will be added to the front of messages of type error, // e.g. "Error: required field is missing". - return gettext("Error"); + return gettext('Error'); } return null; }, @@ -62,13 +62,12 @@ define(["jquery", "underscore", "js/views/baseview", "gettext"], * * @returns string of additional css classes (or empty string) */ - getAdditionalClasses: function () { - if (this.root && this.model.get("summary").type === this.model.NOT_CONFIGURED && - this.model.get("messages").length === 0) { - - return "no-container-content"; + getAdditionalClasses: function() { + if (this.root && this.model.get('summary').type === this.model.NOT_CONFIGURED && + this.model.get('messages').length === 0) { + return 'no-container-content'; } - return ""; + return ''; } }); diff --git a/cms/static/js/xblock/authoring.js b/cms/static/js/xblock/authoring.js index d9abe7d6a6..ffeed552b9 100644 --- a/cms/static/js/xblock/authoring.js +++ b/cms/static/js/xblock/authoring.js @@ -21,7 +21,7 @@ // Cohort partitions (user is allowed to select more than one) element.find('.field-visibility-content-group input:checked').each(function(index, input) { - checkboxValues = $(input).val().split("-"); + checkboxValues = $(input).val().split('-'); partitionId = parseInt(checkboxValues[0], 10); groupId = parseInt(checkboxValues[1], 10); @@ -61,7 +61,7 @@ VisibilityEditorView.prototype.collectFieldData = function collectFieldData() { return { metadata: { - "group_access": this.getGroupAccess() + 'group_access': this.getGroupAccess() } }; }; diff --git a/cms/static/js/xblock_asides/structured_tags.js b/cms/static/js/xblock_asides/structured_tags.js index 2fe124e30a..40e6c3e0a8 100644 --- a/cms/static/js/xblock_asides/structured_tags.js +++ b/cms/static/js/xblock_asides/structured_tags.js @@ -2,10 +2,9 @@ 'use strict'; function StructuredTagsView(runtime, element) { - var $element = $(element); - $element.find("select").each(function() { + $element.find('select').each(function() { var loader = this; var sts = $(this).attr('structured-tags-select-init'); diff --git a/cms/static/karma_cms_squire.conf.js b/cms/static/karma_cms_squire.conf.js index 97361e1a71..d861dc99ca 100644 --- a/cms/static/karma_cms_squire.conf.js +++ b/cms/static/karma_cms_squire.conf.js @@ -41,6 +41,6 @@ var options = { ] }; -module.exports = function (config) { +module.exports = function(config) { configModule.configure(config, options); }; diff --git a/cms/static/sass/_build-v1.scss b/cms/static/sass/_build-v1.scss index 23b66d8db7..b2d56c57ec 100644 --- a/cms/static/sass/_build-v1.scss +++ b/cms/static/sass/_build-v1.scss @@ -68,6 +68,7 @@ @import 'views/group-configuration'; @import 'views/video-upload'; @import 'views/certificates'; +@import 'views/maintenance'; // +Base - Contexts // ==================== diff --git a/cms/static/sass/views/_maintenance.scss b/cms/static/sass/views/_maintenance.scss new file mode 100644 index 0000000000..8f18be8246 --- /dev/null +++ b/cms/static/sass/views/_maintenance.scss @@ -0,0 +1,71 @@ +.maintenance-header { + text-align: center; + margin-top: 50px; + + h2 { + margin-bottom: 10px; + } +} +.maintenance-content { + padding: 3rem 0; + .maintenance-list { + max-width: 1280px; + margin: 0 auto; + .view-list-container { + padding: 10px 15px; + background-color: #fff; + border-bottom: 1px solid #ddd; + &:hover { + background-color: #fafafa; + } + .view-name { + display: inline-block; + width: 20%; + float: left; + } + .view-desc { + display: inline-block; + width: 80%; + font-size: 15px; + } + } + } + .maintenance-form { + width: 60%; + margin: auto; + .result-list { + height: calc(100vh - 200px); + overflow: auto; + } + .result{ + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2); + margin-top: 15px; + padding: 15px 30px; + background: #f9f9f9; + } + li { + font-size: 13px; + line-height: 9px; + } + .actions { + text-align: right; + } + .field-radio div { + display: inline-block; + margin-right: 10px; + } + div.error { + color: #F00; + margin-top: 10px; + font-size: 13px; + } + div.head-output { + font-size: 13px; + margin-bottom: 10px; + } + div.main-output { + color: #0A0; + font-size: 15px; + } + } +} diff --git a/cms/templates/js/maintenance/force-published-course-response.underscore b/cms/templates/js/maintenance/force-published-course-response.underscore new file mode 100644 index 0000000000..d77059ca53 --- /dev/null +++ b/cms/templates/js/maintenance/force-published-course-response.underscore @@ -0,0 +1,14 @@ +
+
+ <%- gettext('You have done a dry run of force publishing the course. Nothing has changed. Had you run it, the following course versions would have been change.') %> +
+
+ <%= StringUtils.interpolate( + gettext('The published branch version, {published}, was reset to the draft branch version, {draft}.'), + { + published: current_versions['published-branch'], + draft: current_versions['draft-branch'] + }) + %> +
+
diff --git a/cms/templates/maintenance/_force_publish_course.html b/cms/templates/maintenance/_force_publish_course.html new file mode 100644 index 0000000000..8066e1e40a --- /dev/null +++ b/cms/templates/maintenance/_force_publish_course.html @@ -0,0 +1,33 @@ +<%page expression_filter="h"/> +<%namespace name='static' file='../static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> +
+
+ +
+
+ ${_("Required data to force publish course.")} +
+
+ + +
${_('course-v1:edX+DemoX+Demo_Course')}
+
+
+
+
+
+
+ + + +
+
+
+
+
diff --git a/cms/templates/maintenance/base.html b/cms/templates/maintenance/base.html new file mode 100644 index 0000000000..d0691063c2 --- /dev/null +++ b/cms/templates/maintenance/base.html @@ -0,0 +1,21 @@ +<%page expression_filter="h"/> +<%inherit file="../base.html" /> +<%def name='online_help_token()'><% return 'maintenance' %> +<%namespace name='static' file='../static_content.html'/> +<%! +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +%> +<%block name="content"> +
+
+

+ + ${_('Maintenance Dashboard')} + +

+ <%block name="viewtitle"> + +
+<%block name="viewcontent"> + diff --git a/cms/templates/maintenance/container.html b/cms/templates/maintenance/container.html new file mode 100644 index 0000000000..c72f6b00e4 --- /dev/null +++ b/cms/templates/maintenance/container.html @@ -0,0 +1,33 @@ +<%page expression_filter="h"/> +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> +<%! +from django.core.urlresolvers import reverse +from openedx.core.djangolib.js_utils import js_escaped_string +%> +<%block name="title">${view['name']} +<%block name="viewtitle"> +

+ ${view['name']} +

+ + +<%block name="viewcontent"> +
+ <%include file="_${view['slug']}.html"/> +
+ + +<%block name="header_extras"> +% for template_name in ["force-published-course-response"]: + +% endfor + + +<%block name="requirejs"> + require(["js/maintenance/${view['slug'] | n, js_escaped_string}"], function(MaintenanceFactory) { + MaintenanceFactory("${reverse(view['url']) | n, js_escaped_string}"); + }); + diff --git a/cms/templates/maintenance/index.html b/cms/templates/maintenance/index.html new file mode 100644 index 0000000000..f1f3224fe1 --- /dev/null +++ b/cms/templates/maintenance/index.html @@ -0,0 +1,20 @@ +<%page expression_filter="h"/> +<%inherit file="base.html" /> +<%namespace name='static' file='../static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +%> +<%block name="title">${_('Maintenance Dashboard')} +<%block name="viewcontent"> +
+ +
+ diff --git a/cms/templates/widgets/user_dropdown.html b/cms/templates/widgets/user_dropdown.html index bebc2e21d0..773c43d5e0 100644 --- a/cms/templates/widgets/user_dropdown.html +++ b/cms/templates/widgets/user_dropdown.html @@ -4,6 +4,7 @@ from django.conf import settings from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ + from student.roles import GlobalStaff %> % if uses_pattern_library: @@ -12,7 +13,7 @@ ${_("Currently signed in as:")} ${ user.username } - ' - ].join(''); + '' + ].join(''); // VideoControl() function - what this module "exports". - return function (state) { - var dfd = $.Deferred(); + return function(state) { + var dfd = $.Deferred(); - state.videoFullScreen = {}; + state.videoFullScreen = {}; - _makeFunctionsPublic(state); - _renderElements(state); - _bindHandlers(state); + _makeFunctionsPublic(state); + _renderElements(state); + _bindHandlers(state); - dfd.resolve(); - return dfd.promise(); - }; + dfd.resolve(); + return dfd.promise(); + }; // *************************************************************** // Private functions start here. @@ -31,62 +31,62 @@ define('video/04_video_full_screen.js', ['edx-ui-toolkit/js/utils/html-utils'], // // Functions which will be accessible via 'state' object. When called, these functions will // get the 'state' object as a context. - function _makeFunctionsPublic(state) { - var methodsDict = { - destroy: destroy, - enter: enter, - exitHandler: exitHandler, - exit: exit, - onFullscreenChange: onFullscreenChange, - toggle: toggle, - toggleHandler: toggleHandler, - updateControlsHeight: updateControlsHeight - }; + function _makeFunctionsPublic(state) { + var methodsDict = { + destroy: destroy, + enter: enter, + exitHandler: exitHandler, + exit: exit, + onFullscreenChange: onFullscreenChange, + toggle: toggle, + toggleHandler: toggleHandler, + updateControlsHeight: updateControlsHeight + }; - state.bindTo(methodsDict, state.videoFullScreen, state); - } - - function destroy() { - $(document).off('keyup', this.videoFullScreen.exitHandler); - this.videoFullScreen.fullScreenEl.remove(); - this.el.off({ - 'fullscreen': this.videoFullScreen.onFullscreenChange, - 'destroy': this.videoFullScreen.destroy - }); - if (this.isFullScreen) { - this.videoFullScreen.exit(); + state.bindTo(methodsDict, state.videoFullScreen, state); + } + + function destroy() { + $(document).off('keyup', this.videoFullScreen.exitHandler); + this.videoFullScreen.fullScreenEl.remove(); + this.el.off({ + 'fullscreen': this.videoFullScreen.onFullscreenChange, + 'destroy': this.videoFullScreen.destroy + }); + if (this.isFullScreen) { + this.videoFullScreen.exit(); + } + delete this.videoFullScreen; } - delete this.videoFullScreen; - } // function _renderElements(state) // // Create any necessary DOM elements, attach them, and set their initial configuration. Also // make the created DOM elements available via the 'state' object. Much easier to work this // way - you don't have to do repeated jQuery element selects. - function _renderElements(state) { - state.videoFullScreen.fullScreenEl = $(template); - state.videoFullScreen.sliderEl = state.el.find('.slider'); - state.videoFullScreen.fullScreenState = false; - HtmlUtils.append(state.el.find('.secondary-controls'), HtmlUtils.HTML(state.videoFullScreen.fullScreenEl)); - state.videoFullScreen.updateControlsHeight(); - } + function _renderElements(state) { + state.videoFullScreen.fullScreenEl = $(template); + state.videoFullScreen.sliderEl = state.el.find('.slider'); + state.videoFullScreen.fullScreenState = false; + HtmlUtils.append(state.el.find('.secondary-controls'), HtmlUtils.HTML(state.videoFullScreen.fullScreenEl)); + state.videoFullScreen.updateControlsHeight(); + } // function _bindHandlers(state) // // Bind any necessary function callbacks to DOM events (click, mousemove, etc.). - function _bindHandlers(state) { - state.videoFullScreen.fullScreenEl.on('click', state.videoFullScreen.toggleHandler); - state.el.on({ - 'fullscreen': state.videoFullScreen.onFullscreenChange, - 'destroy': state.videoFullScreen.destroy - }); - $(document).on('keyup', state.videoFullScreen.exitHandler); - } + function _bindHandlers(state) { + state.videoFullScreen.fullScreenEl.on('click', state.videoFullScreen.toggleHandler); + state.el.on({ + 'fullscreen': state.videoFullScreen.onFullscreenChange, + 'destroy': state.videoFullScreen.destroy + }); + $(document).on('keyup', state.videoFullScreen.exitHandler); + } - function _getControlsHeight(controls, slider) { - return controls.height() + 0.5 * slider.height(); - } + function _getControlsHeight(controls, slider) { + return controls.height() + 0.5 * slider.height(); + } // *************************************************************** // Public functions start here. @@ -94,101 +94,99 @@ define('video/04_video_full_screen.js', ['edx-ui-toolkit/js/utils/html-utils'], // The magic private function that makes them available and sets up their context is makeFunctionsPublic(). // *************************************************************** - function onFullscreenChange (event, isFullScreen) { - var height = this.videoFullScreen.updateControlsHeight(); + function onFullscreenChange(event, isFullScreen) { + var height = this.videoFullScreen.updateControlsHeight(); - if (isFullScreen) { - this.resizer + if (isFullScreen) { + this.resizer .delta .substract(height, 'height') .setMode('both'); - - } else { - this.resizer + } else { + this.resizer .delta .reset() .setMode('width'); + } } - } - function updateControlsHeight() { - var controls = this.el.find('.video-controls'), - slider = this.videoFullScreen.sliderEl; - this.videoFullScreen.height = _getControlsHeight(controls, slider); - return this.videoFullScreen.height; - } + function updateControlsHeight() { + var controls = this.el.find('.video-controls'), + slider = this.videoFullScreen.sliderEl; + this.videoFullScreen.height = _getControlsHeight(controls, slider); + return this.videoFullScreen.height; + } /** * Event handler to toggle fullscreen mode. * @param {jquery Event} event */ - function toggleHandler(event) { - event.preventDefault(); - this.videoCommands.execute('toggleFullScreen'); - } + function toggleHandler(event) { + event.preventDefault(); + this.videoCommands.execute('toggleFullScreen'); + } - function exit() { - var fullScreenClassNameEl = this.el.add(document.documentElement), - closedCaptionsEl = this.el.find('.closed-captions'); + function exit() { + var fullScreenClassNameEl = this.el.add(document.documentElement), + closedCaptionsEl = this.el.find('.closed-captions'); - this.videoFullScreen.fullScreenState = this.isFullScreen = false; - fullScreenClassNameEl.removeClass('video-fullscreen'); - $(window).scrollTop(this.scrollPos); - this.videoFullScreen.fullScreenEl + this.videoFullScreen.fullScreenState = this.isFullScreen = false; + fullScreenClassNameEl.removeClass('video-fullscreen'); + $(window).scrollTop(this.scrollPos); + this.videoFullScreen.fullScreenEl .attr('title', gettext('Fill browser')) .find('.icon') .removeClass('fa-compress') .addClass('fa-arrows-alt'); - this.el.trigger('fullscreen', [this.isFullScreen]); + this.el.trigger('fullscreen', [this.isFullScreen]); - $(closedCaptionsEl).css({ - 'top': '70%', - 'left': '5%' - }); - } + $(closedCaptionsEl).css({ + 'top': '70%', + 'left': '5%' + }); + } - function enter() { - var fullScreenClassNameEl = this.el.add(document.documentElement), - closedCaptionsEl = this.el.find('.closed-captions'); + function enter() { + var fullScreenClassNameEl = this.el.add(document.documentElement), + closedCaptionsEl = this.el.find('.closed-captions'); - this.scrollPos = $(window).scrollTop(); - $(window).scrollTop(0); - this.videoFullScreen.fullScreenState = this.isFullScreen = true; - fullScreenClassNameEl.addClass('video-fullscreen'); - this.videoFullScreen.fullScreenEl + this.scrollPos = $(window).scrollTop(); + $(window).scrollTop(0); + this.videoFullScreen.fullScreenState = this.isFullScreen = true; + fullScreenClassNameEl.addClass('video-fullscreen'); + this.videoFullScreen.fullScreenEl .attr('title', gettext('Exit full browser')) .find('.icon') .removeClass('fa-arrows-alt') .addClass('fa-compress'); - this.el.trigger('fullscreen', [this.isFullScreen]); + this.el.trigger('fullscreen', [this.isFullScreen]); - $(closedCaptionsEl).css({ - 'top': '70%', - 'left': '5%' - }); - } + $(closedCaptionsEl).css({ + 'top': '70%', + 'left': '5%' + }); + } /** Toggle fullscreen mode. */ - function toggle() { - if (this.videoFullScreen.fullScreenState) { - this.videoFullScreen.exit(); - } else { - this.videoFullScreen.enter(); + function toggle() { + if (this.videoFullScreen.fullScreenState) { + this.videoFullScreen.exit(); + } else { + this.videoFullScreen.enter(); + } } - } /** * Event handler to exit from fullscreen mode. * @param {jquery Event} event */ - function exitHandler(event) { - if ((this.isFullScreen) && (event.keyCode === 27)) { - event.preventDefault(); - this.videoCommands.execute('toggleFullScreen'); + function exitHandler(event) { + if ((this.isFullScreen) && (event.keyCode === 27)) { + event.preventDefault(); + this.videoCommands.execute('toggleFullScreen'); + } } - } -}); - + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js b/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js index 074cded354..c9604fe2ca 100644 --- a/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/05_video_quality_control.js @@ -1,23 +1,22 @@ -(function (requirejs, require, define) { - +(function(requirejs, require, define) { // VideoQualityControl module. -'use strict'; -define( + 'use strict'; + define( 'video/05_video_quality_control.js', ['edx-ui-toolkit/js/utils/html-utils'], -function (HtmlUtils) { +function(HtmlUtils) { var template = HtmlUtils.interpolateHtml( HtmlUtils.HTML([ '' ].join('')), { @@ -27,7 +26,7 @@ function (HtmlUtils) { ); // VideoQualityControl() function - what this module "exports". - return function (state) { + return function(state) { var dfd = $.Deferred(); // Changing quality for now only works for YouTube videos. @@ -159,7 +158,6 @@ function (HtmlUtils) { .removeClass('active') .find('.control-text') .text(controlStateStr); - } } @@ -175,7 +173,5 @@ function (HtmlUtils) { this.trigger('videoPlayer.handlePlaybackQualityChange', newQuality); } - }); - }(RequireJS.requirejs, RequireJS.require, RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js index f29ca7697d..b79a17fefe 100644 --- a/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js +++ b/common/lib/xmodule/xmodule/js/src/video/06_video_progress_slider.js @@ -1,5 +1,4 @@ -(function (requirejs, require, define) { - +(function(requirejs, require, define) { /* "This is as true in everyday life as it is in battle: we are given one life and the decision is ours whether to wait for circumstances to make up our @@ -8,16 +7,16 @@ mind, or whether to act, and in acting, to live." */ // VideoProgressSlider module. -define( + define( 'video/06_video_progress_slider.js', [], -function () { +function() { var template = [ '
' ].join(''); // VideoProgressSlider() function - what this module "exports". - return function (state) { + return function(state) { var dfd = $.Deferred(); state.videoProgressSlider = {}; @@ -298,19 +297,19 @@ function () { var seconds = Math.floor(time), minutes = Math.floor(seconds / 60), hours = Math.floor(minutes / 60), - i18n = function (value, word) { + i18n = function(value, word) { var msg; - switch(word) { - case 'hour': - msg = ngettext('%(value)s hour', '%(value)s hours', value); - break; - case 'minute': - msg = ngettext('%(value)s minute', '%(value)s minutes', value); - break; - case 'second': - msg = ngettext('%(value)s second', '%(value)s seconds', value); - break; + switch (word) { + case 'hour': + msg = ngettext('%(value)s hour', '%(value)s hours', value); + break; + case 'minute': + msg = ngettext('%(value)s minute', '%(value)s minutes', value); + break; + case 'second': + msg = ngettext('%(value)s second', '%(value)s seconds', value); + break; } return interpolate(msg, {'value': value}, true); }; @@ -319,17 +318,15 @@ function () { minutes = minutes % 60; if (hours) { - return i18n(hours, 'hour') + ' ' + + return i18n(hours, 'hour') + ' ' + i18n(minutes, 'minute') + ' ' + i18n(seconds, 'second'); } else if (minutes) { - return i18n(minutes, 'minute') + ' ' + + return i18n(minutes, 'minute') + ' ' + i18n(seconds, 'second'); } return i18n(seconds, 'second'); } - }); - }(RequireJS.requirejs, RequireJS.require, RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js index 58c4958d11..9ac4fa3d6b 100644 --- a/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/07_video_volume_control.js @@ -1,9 +1,9 @@ -(function (define) { -'use strict'; +(function(define) { + 'use strict'; // VideoVolumeControl module. -define( + define( 'video/07_video_volume_control.js', ['edx-ui-toolkit/js/utils/html-utils'], -function (HtmlUtils) { +function(HtmlUtils) { /** * Video volume control module. * @exports video/07_video_volume_control.js @@ -39,30 +39,30 @@ function (HtmlUtils) { videoVolumeControlHtml: HtmlUtils.interpolateHtml( HtmlUtils.HTML([ - '
', + '
', '

', - '{volumeInstructions}', + '{volumeInstructions}', '

', '', '', - '
'].join('')), + '
'].join('')), { volumeInstructions: gettext('Click on this button to mute or unmute this video or press UP or DOWN buttons to increase or decrease volume level.'), // eslint-disable-line max-len adjustVideoVolume: gettext('Adjust video volume'), @@ -70,7 +70,7 @@ function (HtmlUtils) { } ), - destroy: function () { + destroy: function() { this.volumeSlider.slider('destroy'); this.state.el.find('iframe').removeAttr('tabindex'); this.a11y.destroy(); @@ -230,7 +230,7 @@ function (HtmlUtils) { }, /** Updates volume slider view. */ - updateSliderView: function (volume) { + updateSliderView: function(volume) { this.volumeSlider.slider('value', volume); this.el.find('.volume-slider') .attr('aria-valuenow', volume); @@ -259,7 +259,7 @@ function (HtmlUtils) { * Returns current volume state (is it muted or not?). * @return {Boolean} */ - getMuteStatus: function () { + getMuteStatus: function() { return this.getVolume() === 0; }, @@ -295,12 +295,12 @@ function (HtmlUtils) { * volume level. * @param {Number} volume Volume level. */ - checkMuteButtonStatus: function (volume) { + checkMuteButtonStatus: function(volume) { if (volume <= this.min) { this.updateMuteButtonView(true); this.state.el.off('volumechange.is-muted'); - this.state.el.on('volumechange.is-muted', _.once(function () { - this.updateMuteButtonView(false); + this.state.el.on('volumechange.is-muted', _.once(function() { + this.updateMuteButtonView(false); }.bind(this))); } }, @@ -336,35 +336,35 @@ function (HtmlUtils) { keyCode = event.keyCode; switch (keyCode) { - case KEY.UP: + case KEY.UP: // Shift + Arrows keyboard shortcut might be used by // screen readers. In this case, do nothing. - if (event.shiftKey) { - return true; - } + if (event.shiftKey) { + return true; + } - this.increaseVolume(); - return false; - case KEY.DOWN: + this.increaseVolume(); + return false; + case KEY.DOWN: // Shift + Arrows keyboard shortcut might be used by // screen readers. In this case, do nothing. - if (event.shiftKey) { - return true; - } + if (event.shiftKey) { + return true; + } - this.decreaseVolume(); - return false; + this.decreaseVolume(); + return false; - case KEY.SPACE: - case KEY.ENTER: + case KEY.SPACE: + case KEY.ENTER: // Shift + Enter keyboard shortcut might be used by // screen readers. In this case, do nothing. - if (event.shiftKey) { - return true; - } + if (event.shiftKey) { + return true; + } - this.toggleMute(); - return false; + this.toggleMute(); + return false; } return true; @@ -374,7 +374,7 @@ function (HtmlUtils) { * Keydown event handler for the volume button. * @param {jquery Event} event */ - keyDownButtonHandler: function(event) { + keyDownButtonHandler: function(event) { // ALT key is used to change (alternate) the function of // other pressed keys. In this case, do nothing. if (event.altKey) { @@ -385,10 +385,10 @@ function (HtmlUtils) { keyCode = event.keyCode; switch (keyCode) { - case KEY.ENTER: - case KEY.SPACE: - this.toggleMute(); - return false; + case KEY.ENTER: + case KEY.SPACE: + this.toggleMute(); + return false; } return true; @@ -433,7 +433,7 @@ function (HtmlUtils) { * @param {Number} max Maximum value for the volume slider. * @param {Object} i18n The object containing strings with translations. */ - var Accessibility = function (button, min, max, i18n) { + var Accessibility = function(button, min, max, i18n) { this.min = min; this.max = max; this.button = button; @@ -443,14 +443,14 @@ function (HtmlUtils) { }; Accessibility.prototype = { - destroy: function () { + destroy: function() { this.liveRegion.remove(); }, /** Initializes the module. */ initialize: function() { this.liveRegion = $('
', { - 'class': 'sr video-live-region', + 'class': 'sr video-live-region', 'aria-hidden': 'false', 'aria-live': 'polite' }); @@ -502,7 +502,7 @@ function (HtmlUtils) { * @param {Number} min Minimum value for the volume slider. * @param {Number} max Maximum value for the volume slider. */ - var CookieManager = function (min, max) { + var CookieManager = function(min, max) { this.min = min; this.max = max; this.cookieName = 'video_player_volume_level'; diff --git a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js index 914896293b..cfc328dea0 100644 --- a/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/08_video_speed_control.js @@ -1,10 +1,10 @@ -(function (requirejs, require, define) { -"use strict"; -define( +(function(requirejs, require, define) { + 'use strict'; + define( 'video/08_video_speed_control.js', [ 'video/00_iterator.js', 'edx-ui-toolkit/js/utils/html-utils' -], function (Iterator, HtmlUtils) { +], function(Iterator, HtmlUtils) { /** * Video speed control module. * @exports video/08_video_speed_control.js @@ -12,7 +12,7 @@ define( * @param {object} state The object containing the state of the video player. * @return {jquery Promise} */ - var SpeedControl = function (state) { + var SpeedControl = function(state) { if (!(this instanceof SpeedControl)) { return new SpeedControl(state); } @@ -31,26 +31,26 @@ define( SpeedControl.prototype = { template: [ '' ].join(''), - destroy: function () { + destroy: function() { this.el.off({ 'mouseenter': this.mouseEnterHandler, 'mouseleave': this.mouseLeaveHandler, @@ -69,7 +69,7 @@ define( }, /** Initializes the module. */ - initialize: function () { + initialize: function() { var state = this.state; if (!this.isPlaybackRatesSupported(state)) { @@ -95,19 +95,19 @@ define( * @param {array} speeds List of speeds available for the player. * @param {string} currentSpeed The current speed set to the player. */ - render: function (speeds, currentSpeed) { + render: function(speeds, currentSpeed) { var speedsContainer = this.speedsContainer, reversedSpeeds = speeds.concat().reverse(), - speedsList = $.map(reversedSpeeds, function (speed) { + speedsList = $.map(reversedSpeeds, function(speed) { return HtmlUtils.interpolateHtml( HtmlUtils.HTML( - [ - '
  • ', - '', - '
  • ' - ].join('') + [ + '
  • ', + '', + '
  • ' + ].join('') ), { speed: speed @@ -131,7 +131,7 @@ define( * Bind any necessary function callbacks to DOM events (click, * mousemove, etc.). */ - bindHandlers: function () { + bindHandlers: function() { // Attach various events handlers to the speed menu button. this.el.on({ 'mouseenter': this.mouseEnterHandler, @@ -154,11 +154,11 @@ define( this.state.el.on('destroy', this.destroy); }, - onSetSpeed: function (event, speed) { + onSetSpeed: function(event, speed) { this.setSpeed(speed, true); }, - onRenderSpeed: function (event, speeds, currentSpeed) { + onRenderSpeed: function(event, speeds, currentSpeed) { this.render(speeds, currentSpeed); }, @@ -173,7 +173,7 @@ define( * true: Browser support playbackRate functionality. * false: Browser doesn't support playbackRate functionality. */ - isPlaybackRatesSupported: function (state) { + isPlaybackRatesSupported: function(state) { var isHtml5 = state.videoType === 'html5', isTouch = state.isTouch, video = document.createElement('video'); @@ -185,7 +185,7 @@ define( * Opens speed menu. * @param {boolean} [bindEvent] Click event will be attached on window. */ - openMenu: function (bindEvent) { + openMenu: function(bindEvent) { // When speed entries have focus, the menu stays open on // mouseleave. A clickHandler is added to the window // element to have clicks close the menu when they happen @@ -204,7 +204,7 @@ define( * Closes speed menu. * @param {boolean} [unBindEvent] Click event will be detached from window. */ - closeMenu: function (unBindEvent) { + closeMenu: function(unBindEvent) { // Remove the previously added clickHandler from window element. if (unBindEvent) { $(window).off('click.speedMenu'); @@ -225,7 +225,7 @@ define( * @param {boolean} [forceUpdate] Updates the speed even if it's * not differs from current speed. */ - setSpeed: function (speed, silent, forceUpdate) { + setSpeed: function(speed, silent, forceUpdate) { if (speed !== this.currentSpeed || forceUpdate) { this.speedsContainer .find('li') @@ -267,7 +267,7 @@ define( * Click event handler for the menu. * @param {jquery Event} event */ - clickMenuHandler: function () { + clickMenuHandler: function() { this.closeMenu(); return false; @@ -277,7 +277,7 @@ define( * Click event handler for speed links. * @param {jquery Event} event */ - clickLinkHandler: function (event) { + clickLinkHandler: function(event) { var el = $(event.currentTarget).parent(), speed = $(el).data('speed'); @@ -293,7 +293,7 @@ define( * Mouseenter event handler for the menu. * @param {jquery Event} event */ - mouseEnterHandler: function () { + mouseEnterHandler: function() { this.openMenu(); return false; @@ -303,12 +303,12 @@ define( * Mouseleave event handler for the menu. * @param {jquery Event} event */ - mouseLeaveHandler: function () { + mouseLeaveHandler: function() { // Only close the menu is no speed entry has focus. if (!this.speedLinks.list.is(':focus')) { this.closeMenu(); } -          + return false; }, @@ -316,33 +316,33 @@ define( * Keydown event handler for the menu. * @param {jquery Event} event */ - keyDownMenuHandler: function (event) { + keyDownMenuHandler: function(event) { var KEY = $.ui.keyCode, keyCode = event.keyCode; - switch(keyCode) { + switch (keyCode) { // Open menu and focus on last element of list above it. - case KEY.ENTER: - case KEY.SPACE: - case KEY.UP: - this.openMenu(true); - this.speedLinks.last().focus(); - break; + case KEY.ENTER: + case KEY.SPACE: + case KEY.UP: + this.openMenu(true); + this.speedLinks.last().focus(); + break; // Close menu. - case KEY.ESCAPE: - this.closeMenu(true); - break; + case KEY.ESCAPE: + this.closeMenu(true); + break; } // We do not stop propagation and default behavior on a TAB // keypress. return event.keyCode === KEY.TAB; -     }, +      }, /** * Keydown event handler for speed links. * @param {jquery Event} event */ - keyDownLinkHandler: function (event) { + keyDownLinkHandler: function(event) { // ALT key is used to change (alternate) the function of // other pressed keys. In this, do nothing. if (event.altKey) { @@ -357,57 +357,56 @@ define( switch (event.keyCode) { // Close menu. - case KEY.TAB: + case KEY.TAB: // Closes menu after 25ms delay to change `tabindex` after // finishing default behavior. - setTimeout(function () { - self.closeMenu(true); - }, 25); + setTimeout(function() { + self.closeMenu(true); + }, 25); - return true; + return true; // Close menu and give focus to speed control. - case KEY.ESCAPE: - this.closeMenu(true); - this.speedButton.focus(); + case KEY.ESCAPE: + this.closeMenu(true); + this.speedButton.focus(); - return false; + return false; // Scroll up menu, wrapping at the top. Keep menu open. - case KEY.UP: + case KEY.UP: // Shift + Arrows keyboard shortcut might be used by // screen readers. In this, do nothing. - if (event.shiftKey) { - return true; - } + if (event.shiftKey) { + return true; + } - this.speedLinks.prev(index).focus(); - return false; + this.speedLinks.prev(index).focus(); + return false; // Scroll down menu, wrapping at the bottom. Keep menu // open. - case KEY.DOWN: + case KEY.DOWN: // Shift + Arrows keyboard shortcut might be used by // screen readers. In this, do nothing. - if (event.shiftKey) { - return true; - } + if (event.shiftKey) { + return true; + } - this.speedLinks.next(index).focus(); - return false; + this.speedLinks.next(index).focus(); + return false; // Close menu, give focus to speed control and change // speed. - case KEY.ENTER: - case KEY.SPACE: - this.closeMenu(true); - this.speedButton.focus(); - this.setSpeed(this.state.speedToString(speed)); + case KEY.ENTER: + case KEY.SPACE: + this.closeMenu(true); + this.speedButton.focus(); + this.setSpeed(this.state.speedToString(speed)); - return false; + return false; } return true; -     } +      } }; return SpeedControl; }); - }(RequireJS.requirejs, RequireJS.require, RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js b/common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js index 80e0e9a1d1..cfffa0a6de 100644 --- a/common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js +++ b/common/lib/xmodule/xmodule/js/src/video/095_video_context_menu.js @@ -1,14 +1,14 @@ -(function (define) { -'use strict'; +(function(define) { + 'use strict'; // VideoContextMenu module. -define( + define( 'video/095_video_context_menu.js', ['video/00_component.js'], -function (Component) { +function(Component) { var AbstractItem, AbstractMenu, Menu, Overlay, Submenu, MenuItem; AbstractItem = Component.extend({ - initialize: function (options) { + initialize: function(options) { this.options = $.extend(true, { label: '', prefix: 'edx-', @@ -26,24 +26,24 @@ function (Component) { this.delegateEvents(); this.options.initialize.call(this, this); }, - destroy: function () { + destroy: function() { _.invoke(this.getChildren(), 'destroy'); this.undelegateEvents(); this.getElement().remove(); }, - open: function () { + open: function() { this.getElement().addClass('is-opened'); return this; }, - close: function () { }, - closeSiblings: function () { + close: function() { }, + closeSiblings: function() { _.invoke(this.getSiblings(), 'close'); return this; }, - getElement: function () { + getElement: function() { return this.element; }, - addChild: function (child) { + addChild: function(child) { var firstChild = null, lastChild = null; if (this.hasChildren()) { lastChild = this.getLastChild(); @@ -57,51 +57,51 @@ function (Component) { this.children.push(child); return this; }, - getChildren: function () { + getChildren: function() { // Returns the copy. return this.children.concat(); }, - hasChildren: function () { + hasChildren: function() { return this.getChildren().length > 0; }, - getFirstChild: function () { + getFirstChild: function() { return _.first(this.children); }, - getLastChild: function () { + getLastChild: function() { return _.last(this.children); }, - bindEvent: function (element, events, handler) { + bindEvent: function(element, events, handler) { $(element).on(this.addNamespace(events), handler); return this; }, - getNext: function () { + getNext: function() { var item = this.next; while (item.isHidden() && this.id !== item.id) { item = item.next; } return item; }, - getPrev: function () { + getPrev: function() { var item = this.prev; while (item.isHidden() && this.id !== item.id) { item = item.prev; } return item; }, - createElement: function () { + createElement: function() { return null; }, - getRoot: function () { + getRoot: function() { var item = this; while (item.parent) { item = item.parent; } return item; }, - populateElement: function () { }, - focus: function () { + populateElement: function() { }, + focus: function() { this.getElement().focus(); this.closeSiblings(); return this; }, - isHidden: function () { + isHidden: function() { return this.getElement().is(':hidden'); }, - getSiblings: function () { + getSiblings: function() { var items = [], item = this; while (item.next && item.next.id !== this.id) { @@ -110,33 +110,33 @@ function (Component) { } return items; }, - select: function () { }, - unselect: function () { }, - setLabel: function () { }, - itemHandler: function () { }, - keyDownHandler: function () { }, - delegateEvents: function () { }, - undelegateEvents: function () { + select: function() { }, + unselect: function() { }, + setLabel: function() { }, + itemHandler: function() { }, + keyDownHandler: function() { }, + delegateEvents: function() { }, + undelegateEvents: function() { this.getElement().off('.' + this.id); }, - addNamespace: function (events) { - return _.map(events.split(/\s+/), function (event) { + addNamespace: function(events) { + return _.map(events.split(/\s+/), function(event) { return event + '.' + this.id; }, this).join(' '); } }); AbstractMenu = AbstractItem.extend({ - delegateEvents: function () { + delegateEvents: function() { this.bindEvent(this.getElement(), 'keydown mouseleave mouseover', this.itemHandler.bind(this)) - .bindEvent(this.getElement(), 'contextmenu', function (event) { event.preventDefault(); }); + .bindEvent(this.getElement(), 'contextmenu', function(event) { event.preventDefault(); }); return this; }, - populateElement: function () { + populateElement: function() { var fragment = document.createDocumentFragment(); - _.each(this.getChildren(), function (child) { + _.each(this.getChildren(), function(child) { fragment.appendChild(child.populateElement()[0]); }, this); @@ -145,40 +145,40 @@ function (Component) { return this.getElement(); }, - close: function () { + close: function() { this.closeChildren(); this.getElement().removeClass('is-opened'); return this; }, - closeChildren: function () { + closeChildren: function() { _.invoke(this.getChildren(), 'close'); return this; }, - itemHandler: function (event) { + itemHandler: function(event) { event.preventDefault(); var item = $(event.target).data('menu'); - switch(event.type) { - case 'keydown': - this.keyDownHandler.call(this, event, item); - break; - case 'mouseover': - this.mouseOverHandler.call(this, event, item); - break; - case 'mouseleave': - this.mouseLeaveHandler.call(this, event, item); - break; + switch (event.type) { + case 'keydown': + this.keyDownHandler.call(this, event, item); + break; + case 'mouseover': + this.mouseOverHandler.call(this, event, item); + break; + case 'mouseleave': + this.mouseLeaveHandler.call(this, event, item); + break; } }, - keyDownHandler: function () { }, - mouseOverHandler: function () { }, - mouseLeaveHandler: function () { } + keyDownHandler: function() { }, + mouseOverHandler: function() { }, + mouseLeaveHandler: function() { } }); Menu = AbstractMenu.extend({ - initialize: function (options, contextmenuElement, container) { + initialize: function(options, contextmenuElement, container) { this.contextmenuElement = $(contextmenuElement); this.container = $(container); this.overlay = this.getOverlay(); @@ -186,7 +186,7 @@ function (Component) { this.build(this, this.options.items); }, - createElement: function () { + createElement: function() { return $('
      ', { 'class': ['contextmenu', this.options.prefix + 'contextmenu'].join(' '), 'role': 'menu', @@ -194,40 +194,40 @@ function (Component) { }); }, - delegateEvents: function () { + delegateEvents: function() { AbstractMenu.prototype.delegateEvents.call(this); this.bindEvent(this.contextmenuElement, 'contextmenu', this.contextmenuHandler.bind(this)) .bindEvent(window, 'resize', _.debounce(this.close.bind(this), 100)); return this; }, - destroy: function () { + destroy: function() { AbstractMenu.prototype.destroy.call(this); this.overlay.destroy(); this.contextmenuElement.removeData('contextmenu'); return this; }, - undelegateEvents: function () { + undelegateEvents: function() { AbstractMenu.prototype.undelegateEvents.call(this); this.contextmenuElement.off(this.addNamespace('contextmenu')); this.overlay.undelegateEvents(); return this; }, - appendContent: function (content) { + appendContent: function(content) { this.getElement().append(content); return this; }, - addChild: function () { + addChild: function() { AbstractMenu.prototype.addChild.apply(this, arguments); this.next = this.getFirstChild(); this.prev = this.getLastChild(); return this; }, - build: function (container, items) { + build: function(container, items) { _.each(items, function(item) { var child; if (_.has(item, 'items')) { @@ -240,12 +240,12 @@ function (Component) { return container; }, - focus: function () { + focus: function() { this.getElement().focus(); return this; }, - open: function () { + open: function() { var menu = (this.isRendered) ? this.getElement() : this.populateElement(); this.container.append(menu); AbstractItem.prototype.open.call(this); @@ -253,7 +253,7 @@ function (Component) { return this; }, - close: function () { + close: function() { AbstractMenu.prototype.close.call(this); this.getElement().detach(); this.overlay.hide(); @@ -271,7 +271,7 @@ function (Component) { return this; }, - pointInContainerBox: function (x, y) { + pointInContainerBox: function(x, y) { var containerOffset = this.contextmenuElement.offset(), containerBox = { x0: containerOffset.left, @@ -282,10 +282,10 @@ function (Component) { return containerBox.x0 <= x && x <= containerBox.x1 && containerBox.y0 <= y && y <= containerBox.y1; }, - getOverlay: function () { + getOverlay: function() { return new Overlay( this.close.bind(this), - function (event) { + function(event) { event.preventDefault(); if (this.pointInContainerBox(event.pageX, event.pageY)) { this.position(event).focus(); @@ -293,45 +293,44 @@ function (Component) { } else { this.close(); } - }.bind(this) ); }, - contextmenuHandler: function (event) { + contextmenuHandler: function(event) { event.preventDefault(); event.stopPropagation(); this.open().position(event).focus(); }, - keyDownHandler: function (event, item) { + keyDownHandler: function(event, item) { var KEY = $.ui.keyCode, keyCode = event.keyCode; switch (keyCode) { - case KEY.UP: - item.getPrev().focus(); - event.stopPropagation(); - break; - case KEY.DOWN: - item.getNext().focus(); - event.stopPropagation(); - break; - case KEY.TAB: - event.stopPropagation(); - break; - case KEY.ESCAPE: - this.close(); - break; + case KEY.UP: + item.getPrev().focus(); + event.stopPropagation(); + break; + case KEY.DOWN: + item.getNext().focus(); + event.stopPropagation(); + break; + case KEY.TAB: + event.stopPropagation(); + break; + case KEY.ESCAPE: + this.close(); + break; } return false; -     } +      } }); Overlay = Component.extend({ ns: '.overlay', - initialize: function (clickHandler, contextmenuHandler) { + initialize: function(clickHandler, contextmenuHandler) { this.element = $('
      ', { 'class': 'overlay' }); @@ -339,37 +338,37 @@ function (Component) { this.contextmenuHandler = contextmenuHandler; }, - destroy: function () { + destroy: function() { this.getElement().remove(); this.undelegateEvents(); }, - getElement: function () { + getElement: function() { return this.element; }, - hide: function () { + hide: function() { this.getElement().detach(); this.undelegateEvents(); return this; }, - show: function (container) { + show: function(container) { $(container).append(this.getElement()); this.delegateEvents(); return this; }, - delegateEvents: function () { + delegateEvents: function() { var self = this; $(document) - .on('click' + this.ns, function () { + .on('click' + this.ns, function() { if (_.isFunction(self.clickHandler)) { self.clickHandler.apply(this, arguments); } self.hide(); }) - .on('contextmenu' + this.ns, function () { + .on('contextmenu' + this.ns, function() { if (_.isFunction(self.contextmenuHandler)) { self.contextmenuHandler.apply(this, arguments); } @@ -377,21 +376,21 @@ function (Component) { return this; }, - undelegateEvents: function () { + undelegateEvents: function() { $(document).off(this.ns); return this; } }); Submenu = AbstractMenu.extend({ - initialize: function (options, contextmenuElement) { + initialize: function(options, contextmenuElement) { this.contextmenuElement = contextmenuElement; AbstractMenu.prototype.initialize.apply(this, arguments); }, - createElement: function () { + createElement: function() { var element = $('
    1. ', { - 'class': ['submenu-item','menu-item', this.options.prefix + 'submenu-item'].join(' '), + 'class': ['submenu-item', 'menu-item', this.options.prefix + 'submenu-item'].join(' '), 'aria-expanded': 'false', 'aria-haspopup': 'true', 'aria-labelledby': 'submenu-item-label-' + this.id, @@ -412,17 +411,17 @@ function (Component) { return element; }, - appendContent: function (content) { + appendContent: function(content) { this.list.append(content); return this; }, - setLabel: function (label) { + setLabel: function(label) { this.label.text(label); return this; }, - openKeyboard: function () { + openKeyboard: function() { if (this.hasChildren()) { this.open(); this.getFirstChild().focus(); @@ -430,40 +429,40 @@ function (Component) { return this; }, - keyDownHandler: function (event) { + keyDownHandler: function(event) { var KEY = $.ui.keyCode, keyCode = event.keyCode; switch (keyCode) { - case KEY.LEFT: - this.close().focus(); - event.stopPropagation(); - break; - case KEY.RIGHT: - case KEY.ENTER: - case KEY.SPACE: - this.openKeyboard(); - event.stopPropagation(); - break; + case KEY.LEFT: + this.close().focus(); + event.stopPropagation(); + break; + case KEY.RIGHT: + case KEY.ENTER: + case KEY.SPACE: + this.openKeyboard(); + event.stopPropagation(); + break; } return false; -     }, +      }, - open: function () { + open: function() { AbstractMenu.prototype.open.call(this); this.getElement().attr({'aria-expanded': 'true'}); this.position(); return this; }, - close: function () { + close: function() { AbstractMenu.prototype.close.call(this); this.getElement().attr({'aria-expanded': 'false'}); return this; }, - position: function () { + position: function() { this.list.position({ my: 'left top', at: 'right top', @@ -474,13 +473,13 @@ function (Component) { return this; }, - mouseOverHandler: function () { + mouseOverHandler: function() { clearTimeout(this.timer); this.timer = setTimeout(this.open.bind(this), 200); this.focus(); }, - mouseLeaveHandler: function () { + mouseLeaveHandler: function() { clearTimeout(this.timer); this.timer = setTimeout(this.close.bind(this), 200); this.focus(); @@ -488,7 +487,7 @@ function (Component) { }); MenuItem = AbstractItem.extend({ - createElement: function () { + createElement: function() { var classNames = [ 'menu-item', this.options.prefix + 'menu-item', this.options.isSelected ? 'is-selected' : '' @@ -503,21 +502,21 @@ function (Component) { }); }, - populateElement: function () { + populateElement: function() { return this.getElement(); }, - delegateEvents: function () { + delegateEvents: function() { this.bindEvent(this.getElement(), 'click keydown contextmenu mouseover', this.itemHandler.bind(this)); return this; }, - setLabel: function (label) { + setLabel: function(label) { this.getElement().text(label); return this; }, - select: function (event) { + select: function(event) { this.options.callback.call(this, event, this, this.options); this.getElement() .addClass('is-selected') @@ -528,80 +527,79 @@ function (Component) { return this; }, - unselect: function () { + unselect: function() { this.getElement() .removeClass('is-selected') .attr({'aria-selected': 'false'}); return this; }, - itemHandler: function (event) { + itemHandler: function(event) { event.preventDefault(); - switch(event.type) { - case 'contextmenu': - case 'click': - this.select(); - break; - case 'mouseover': - this.focus(); - event.stopPropagation(); - break; - case 'keydown': - this.keyDownHandler.call(this, event, this); - break; + switch (event.type) { + case 'contextmenu': + case 'click': + this.select(); + break; + case 'mouseover': + this.focus(); + event.stopPropagation(); + break; + case 'keydown': + this.keyDownHandler.call(this, event, this); + break; } }, - keyDownHandler: function (event) { + keyDownHandler: function(event) { var KEY = $.ui.keyCode, keyCode = event.keyCode; switch (keyCode) { - case KEY.RIGHT: - event.stopPropagation(); - break; - case KEY.ENTER: - case KEY.SPACE: - this.select(); - event.stopPropagation(); - break; + case KEY.RIGHT: + event.stopPropagation(); + break; + case KEY.ENTER: + case KEY.SPACE: + this.select(); + event.stopPropagation(); + break; } return false; -     } +      } }); // VideoContextMenu() function - what this module 'exports'. - return function (state, i18n) { - - var speedCallback = function (event, menuitem, options) { + return function(state, i18n) { + var speedCallback = function(event, menuitem, options) { var speed = parseFloat(options.label); state.videoCommands.execute('speed', speed); }, options = { items: [{ label: i18n.Play, - callback: function () { + callback: function() { state.videoCommands.execute('togglePlayback'); }, - initialize: function (menuitem) { + initialize: function(menuitem) { state.el.on({ - 'play': function () { + 'play': function() { menuitem.setLabel(i18n.Pause); }, - 'pause': function () { + 'pause': function() { menuitem.setLabel(i18n.Play); } }); } }, { label: state.videoVolumeControl.getMuteStatus() ? i18n.Unmute : i18n.Mute, - callback: function () { + callback: function() { state.videoCommands.execute('toggleMute'); }, - initialize: function (menuitem) { + initialize: function(menuitem) { state.el.on({ - 'volumechange': function () { + 'volumechange': function() { if (state.videoVolumeControl.getMuteStatus()) { menuitem.setLabel(i18n.Unmute); } else { @@ -612,12 +610,12 @@ function (Component) { } }, { label: i18n['Fill browser'], - callback: function () { + callback: function() { state.videoCommands.execute('toggleFullScreen'); }, - initialize: function (menuitem) { + initialize: function(menuitem) { state.el.on({ - 'fullscreen': function (event, isFullscreen) { + 'fullscreen': function(event, isFullscreen) { if (isFullscreen) { menuitem.setLabel(i18n['Exit full browser']); } else { @@ -628,14 +626,14 @@ function (Component) { } }, { label: i18n.Speed, - items: _.map(state.speeds, function (speed) { + items: _.map(state.speeds, function(speed) { var isSelected = speed === state.speed; return {label: speed + 'x', callback: speedCallback, speed: speed, isSelected: isSelected}; }), - initialize: function (menuitem) { + initialize: function(menuitem) { state.el.on({ - 'speedchange': function (event, speed) { - var item = menuitem.getChildren().filter(function (item) { + 'speedchange': function(event, speed) { + var item = menuitem.getChildren().filter(function(item) { return item.options.speed === speed; })[0]; if (item) { @@ -646,9 +644,9 @@ function (Component) { } } ] - }; + }; - $.fn.contextmenu = function (container, options) { + $.fn.contextmenu = function(container, options) { return this.each(function() { $(this).data('contextmenu', new Menu(options, this, container)); }); @@ -656,7 +654,7 @@ function (Component) { if (!state.isYoutubeType()) { state.el.find('video').contextmenu(state.el, options); - state.el.on('destroy', function () { + state.el.on('destroy', function() { var contextmenu = $(this).find('video').data('contextmenu'); if (contextmenu) { contextmenu.destroy(); @@ -667,5 +665,4 @@ function (Component) { return $.Deferred().resolve().promise(); }; }); - }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_bumper.js b/common/lib/xmodule/xmodule/js/src/video/09_bumper.js index 62681fa6b2..1a6f5ddce2 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_bumper.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_bumper.js @@ -1,6 +1,6 @@ -(function (define) { -'use strict'; -define('video/09_bumper.js',[], function () { +(function(define) { + 'use strict'; + define('video/09_bumper.js', [], function() { /** * VideoBumper module. * @exports video/09_bumper.js @@ -9,103 +9,103 @@ define('video/09_bumper.js',[], function () { * @param {Object} state The object containing the state of the video * @return {jquery Promise} */ - var VideoBumper = function (player, state) { - if (!(this instanceof VideoBumper)) { - return new VideoBumper(player, state); - } + var VideoBumper = function(player, state) { + if (!(this instanceof VideoBumper)) { + return new VideoBumper(player, state); + } - _.bindAll( + _.bindAll( this, 'showMainVideoHandler', 'destroy', 'skipByDuration', 'destroyAndResolve' ); - this.dfd = $.Deferred(); - this.element = state.el; - this.element.addClass('is-bumper'); - this.player = player; - this.state = state; - this.doNotShowAgain = false; - this.state.videoBumper = this; - this.bindHandlers(); - this.initialize(); - this.maxBumperDuration = 35; // seconds - }; + this.dfd = $.Deferred(); + this.element = state.el; + this.element.addClass('is-bumper'); + this.player = player; + this.state = state; + this.doNotShowAgain = false; + this.state.videoBumper = this; + this.bindHandlers(); + this.initialize(); + this.maxBumperDuration = 35; // seconds + }; - VideoBumper.prototype = { - initialize: function () { - this.player(); - }, + VideoBumper.prototype = { + initialize: function() { + this.player(); + }, - getPromise: function () { - return this.dfd.promise(); - }, + getPromise: function() { + return this.dfd.promise(); + }, - showMainVideoHandler: function () { - this.state.storage.setItem('isBumperShown', true); - setTimeout(function () { - this.saveState(); - this.showMainVideo(); - }.bind(this), 20); - }, + showMainVideoHandler: function() { + this.state.storage.setItem('isBumperShown', true); + setTimeout(function() { + this.saveState(); + this.showMainVideo(); + }.bind(this), 20); + }, - destroyAndResolve: function () { - this.destroy(); - this.dfd.resolve(); - }, + destroyAndResolve: function() { + this.destroy(); + this.dfd.resolve(); + }, - showMainVideo: function () { - if (this.state.videoPlayer) { - this.destroyAndResolve(); - } else { - this.state.el.on('initialize', this.destroyAndResolve); + showMainVideo: function() { + if (this.state.videoPlayer) { + this.destroyAndResolve(); + } else { + this.state.el.on('initialize', this.destroyAndResolve); + } + }, + + skip: function() { + this.element.trigger('skip', [this.doNotShowAgain]); + this.showMainVideoHandler(); + }, + + skipAndDoNotShowAgain: function() { + this.doNotShowAgain = true; + this.skip(); + }, + + skipByDuration: function(event, time) { + if (time > this.maxBumperDuration) { + this.element.trigger('ended'); + } + }, + + bindHandlers: function() { + var events = ['ended', 'error'].join(' '); + this.element.on(events, this.showMainVideoHandler); + this.element.on('timeupdate', this.skipByDuration); + }, + + saveState: function() { + var info = {bumper_last_view_date: true}; + if (this.doNotShowAgain) { + _.extend(info, {bumper_do_not_show_again: true}); + } + if (this.state.videoSaveStatePlugin) { + this.state.videoSaveStatePlugin.saveState(true, info); + } + }, + + destroy: function() { + var events = ['ended', 'error'].join(' '); + this.element.off(events, this.showMainVideoHandler); + this.element.off({ + 'timeupdate': this.skipByDuration, + 'initialize': this.destroyAndResolve + }); + this.element.removeClass('is-bumper'); + if (_.isFunction(this.state.videoPlayer.destroy)) { + this.state.videoPlayer.destroy(); + } + delete this.state.videoBumper; } - }, + }; - skip: function () { - this.element.trigger('skip', [this.doNotShowAgain]); - this.showMainVideoHandler(); - }, - - skipAndDoNotShowAgain: function () { - this.doNotShowAgain = true; - this.skip(); - }, - - skipByDuration: function (event, time) { - if (time > this.maxBumperDuration) { - this.element.trigger('ended'); - } - }, - - bindHandlers: function () { - var events = ['ended', 'error'].join(' '); - this.element.on(events, this.showMainVideoHandler); - this.element.on('timeupdate', this.skipByDuration); - }, - - saveState: function () { - var info = {bumper_last_view_date: true}; - if (this.doNotShowAgain) { - _.extend(info, {bumper_do_not_show_again: true}); - } - if (this.state.videoSaveStatePlugin) { - this.state.videoSaveStatePlugin.saveState(true, info); - } - }, - - destroy: function () { - var events = ['ended', 'error'].join(' '); - this.element.off(events, this.showMainVideoHandler); - this.element.off({ - 'timeupdate': this.skipByDuration, - 'initialize': this.destroyAndResolve - }); - this.element.removeClass('is-bumper'); - if (_.isFunction(this.state.videoPlayer.destroy)) { - this.state.videoPlayer.destroy(); - } - delete this.state.videoBumper; - } - }; - - return VideoBumper; -}); + return VideoBumper; + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_events_bumper_plugin.js b/common/lib/xmodule/xmodule/js/src/video/09_events_bumper_plugin.js index 4e18332eb2..3eb205101b 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_events_bumper_plugin.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_events_bumper_plugin.js @@ -1,6 +1,6 @@ (function(define) { -'use strict'; -define('video/09_events_bumper_plugin.js', [], function() { + 'use strict'; + define('video/09_events_bumper_plugin.js', [], function() { /** * Events module. * @exports video/09_events_bumper_plugin.js @@ -10,103 +10,103 @@ define('video/09_events_bumper_plugin.js', [], function() { * @param {Object} options * @return {jquery Promise} */ - var EventsBumperPlugin = function(state, i18n, options) { - if (!(this instanceof EventsBumperPlugin)) { - return new EventsBumperPlugin(state, i18n, options); - } + var EventsBumperPlugin = function(state, i18n, options) { + if (!(this instanceof EventsBumperPlugin)) { + return new EventsBumperPlugin(state, i18n, options); + } - _.bindAll(this, 'onReady', 'onPlay', 'onEnded', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip', + _.bindAll(this, 'onReady', 'onPlay', 'onEnded', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip', 'onShowCaptions', 'onHideCaptions', 'destroy'); - this.state = state; - this.options = _.extend({}, options); - this.state.videoEventsBumperPlugin = this; - this.i18n = i18n; - this.initialize(); + this.state = state; + this.options = _.extend({}, options); + this.state.videoEventsBumperPlugin = this; + this.i18n = i18n; + this.initialize(); - return $.Deferred().resolve().promise(); - }; + return $.Deferred().resolve().promise(); + }; - EventsBumperPlugin.moduleName = 'EventsBumperPlugin'; - EventsBumperPlugin.prototype = { - destroy: function () { - this.state.el.off(this.events); - delete this.state.videoEventsBumperPlugin; - }, + EventsBumperPlugin.moduleName = 'EventsBumperPlugin'; + EventsBumperPlugin.prototype = { + destroy: function() { + this.state.el.off(this.events); + delete this.state.videoEventsBumperPlugin; + }, - initialize: function() { - this.events = { - 'ready': this.onReady, - 'play': this.onPlay, - 'ended stop': this.onEnded, - 'skip': this.onSkip, - 'language_menu:show': this.onShowLanguageMenu, - 'language_menu:hide': this.onHideLanguageMenu, - 'captions:show': this.onShowCaptions, - 'captions:hide': this.onHideCaptions, - 'destroy': this.destroy - }; - this.bindHandlers(); - }, + initialize: function() { + this.events = { + 'ready': this.onReady, + 'play': this.onPlay, + 'ended stop': this.onEnded, + 'skip': this.onSkip, + 'language_menu:show': this.onShowLanguageMenu, + 'language_menu:hide': this.onHideLanguageMenu, + 'captions:show': this.onShowCaptions, + 'captions:hide': this.onHideCaptions, + 'destroy': this.destroy + }; + this.bindHandlers(); + }, - bindHandlers: function() { - this.state.el.on(this.events); - }, + bindHandlers: function() { + this.state.el.on(this.events); + }, - onReady: function () { - this.log('edx.video.bumper.loaded'); - }, + onReady: function() { + this.log('edx.video.bumper.loaded'); + }, - onPlay: function () { - this.log('edx.video.bumper.played', {currentTime: this.getCurrentTime()}); - }, + onPlay: function() { + this.log('edx.video.bumper.played', {currentTime: this.getCurrentTime()}); + }, - onEnded: function () { - this.log('edx.video.bumper.stopped', {currentTime: this.getCurrentTime()}); - }, + onEnded: function() { + this.log('edx.video.bumper.stopped', {currentTime: this.getCurrentTime()}); + }, - onSkip: function (event, doNotShowAgain) { - var info = {currentTime: this.getCurrentTime()}, - eventName = 'edx.video.bumper.' + (doNotShowAgain ? 'dismissed': 'skipped'); - this.log(eventName, info); - }, + onSkip: function(event, doNotShowAgain) { + var info = {currentTime: this.getCurrentTime()}, + eventName = 'edx.video.bumper.' + (doNotShowAgain ? 'dismissed' : 'skipped'); + this.log(eventName, info); + }, - onShowLanguageMenu: function () { - this.log('edx.video.bumper.transcript.menu.shown'); - }, + onShowLanguageMenu: function() { + this.log('edx.video.bumper.transcript.menu.shown'); + }, - onHideLanguageMenu: function () { - this.log('edx.video.bumper.transcript.menu.hidden'); - }, + onHideLanguageMenu: function() { + this.log('edx.video.bumper.transcript.menu.hidden'); + }, - onShowCaptions: function () { - this.log('edx.video.bumper.transcript.shown', {currentTime: this.getCurrentTime()}); - }, + onShowCaptions: function() { + this.log('edx.video.bumper.transcript.shown', {currentTime: this.getCurrentTime()}); + }, - onHideCaptions: function () { - this.log('edx.video.bumper.transcript.hidden', {currentTime: this.getCurrentTime()}); - }, + onHideCaptions: function() { + this.log('edx.video.bumper.transcript.hidden', {currentTime: this.getCurrentTime()}); + }, - getCurrentTime: function () { - var player = this.state.videoPlayer; - return player ? player.currentTime : 0; - }, + getCurrentTime: function() { + var player = this.state.videoPlayer; + return player ? player.currentTime : 0; + }, - getDuration: function () { - var player = this.state.videoPlayer; - return player ? player.duration() : 0; - }, + getDuration: function() { + var player = this.state.videoPlayer; + return player ? player.duration() : 0; + }, - log: function (eventName, data) { - var logInfo = _.extend({ - host_component_id: this.state.id, - bumper_id: this.state.config.sources[0] || '', - duration: this.getDuration(), - code: 'html5' - }, data, this.options.data); - Logger.log(eventName, logInfo); - } - }; + log: function(eventName, data) { + var logInfo = _.extend({ + host_component_id: this.state.id, + bumper_id: this.state.config.sources[0] || '', + duration: this.getDuration(), + code: 'html5' + }, data, this.options.data); + Logger.log(eventName, logInfo); + } + }; - return EventsBumperPlugin; -}); + return EventsBumperPlugin; + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js b/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js index 5ed7ed77a2..39818d899b 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js @@ -1,6 +1,6 @@ (function(define) { -'use strict'; -define('video/09_events_plugin.js', [], function() { + 'use strict'; + define('video/09_events_plugin.js', [], function() { /** * Events module. * @exports video/09_events_plugin.js @@ -10,143 +10,143 @@ define('video/09_events_plugin.js', [], function() { * @param {Object} options * @return {jquery Promise} */ - var EventsPlugin = function(state, i18n, options) { - if (!(this instanceof EventsPlugin)) { - return new EventsPlugin(state, i18n, options); - } + var EventsPlugin = function(state, i18n, options) { + if (!(this instanceof EventsPlugin)) { + return new EventsPlugin(state, i18n, options); + } - _.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onEnded', 'onSeek', + _.bindAll(this, 'onReady', 'onPlay', 'onPause', 'onEnded', 'onSeek', 'onSpeedChange', 'onShowLanguageMenu', 'onHideLanguageMenu', 'onSkip', 'onShowTranscript', 'onHideTranscript', 'onShowCaptions', 'onHideCaptions', 'destroy'); - this.state = state; - this.options = _.extend({}, options); - this.state.videoEventsPlugin = this; - this.i18n = i18n; - this.initialize(); + this.state = state; + this.options = _.extend({}, options); + this.state.videoEventsPlugin = this; + this.i18n = i18n; + this.initialize(); - return $.Deferred().resolve().promise(); - }; + return $.Deferred().resolve().promise(); + }; - EventsPlugin.moduleName = 'EventsPlugin'; - EventsPlugin.prototype = { - destroy: function () { - this.state.el.off(this.events); - delete this.state.videoEventsPlugin; - }, + EventsPlugin.moduleName = 'EventsPlugin'; + EventsPlugin.prototype = { + destroy: function() { + this.state.el.off(this.events); + delete this.state.videoEventsPlugin; + }, - initialize: function() { - this.events = { - 'ready': this.onReady, - 'play': this.onPlay, - 'pause': this.onPause, - 'ended stop': this.onEnded, - 'seek': this.onSeek, - 'skip': this.onSkip, - 'speedchange': this.onSpeedChange, - 'language_menu:show': this.onShowLanguageMenu, - 'language_menu:hide': this.onHideLanguageMenu, - 'transcript:show': this.onShowTranscript, - 'transcript:hide': this.onHideTranscript, - 'captions:show': this.onShowCaptions, - 'captions:hide': this.onHideCaptions, - 'destroy': this.destroy - }; - this.bindHandlers(); - this.emitPlayVideoEvent = true; - }, + initialize: function() { + this.events = { + 'ready': this.onReady, + 'play': this.onPlay, + 'pause': this.onPause, + 'ended stop': this.onEnded, + 'seek': this.onSeek, + 'skip': this.onSkip, + 'speedchange': this.onSpeedChange, + 'language_menu:show': this.onShowLanguageMenu, + 'language_menu:hide': this.onHideLanguageMenu, + 'transcript:show': this.onShowTranscript, + 'transcript:hide': this.onHideTranscript, + 'captions:show': this.onShowCaptions, + 'captions:hide': this.onHideCaptions, + 'destroy': this.destroy + }; + this.bindHandlers(); + this.emitPlayVideoEvent = true; + }, - bindHandlers: function() { - this.state.el.on(this.events); - }, + bindHandlers: function() { + this.state.el.on(this.events); + }, - onReady: function () { - this.log('load_video'); - }, + onReady: function() { + this.log('load_video'); + }, - onPlay: function () { - if (this.emitPlayVideoEvent) { - this.log('play_video', {currentTime: this.getCurrentTime()}); - this.emitPlayVideoEvent = false; + onPlay: function() { + if (this.emitPlayVideoEvent) { + this.log('play_video', {currentTime: this.getCurrentTime()}); + this.emitPlayVideoEvent = false; + } + }, + + onPause: function() { + this.log('pause_video', {currentTime: this.getCurrentTime()}); + this.emitPlayVideoEvent = true; + }, + + onEnded: function() { + this.log('stop_video', {currentTime: this.getCurrentTime()}); + this.emitPlayVideoEvent = true; + }, + + onSkip: function(event, doNotShowAgain) { + var info = {currentTime: this.getCurrentTime()}, + eventName = doNotShowAgain ? 'do_not_show_again_video' : 'skip_video'; + this.log(eventName, info); + }, + + onSeek: function(event, time, oldTime, type) { + this.log('seek_video', { + old_time: oldTime, + new_time: time, + type: type + }); + }, + + onSpeedChange: function(event, newSpeed, oldSpeed) { + this.log('speed_change_video', { + current_time: this.getCurrentTime(), + old_speed: oldSpeed, + new_speed: newSpeed + }); + }, + + onShowLanguageMenu: function() { + this.log('edx.video.language_menu.shown'); + }, + + onHideLanguageMenu: function() { + this.log('edx.video.language_menu.hidden', {language: this.getCurrentLanguage()}); + }, + + onShowTranscript: function() { + this.log('show_transcript', {current_time: this.getCurrentTime()}); + }, + + onHideTranscript: function() { + this.log('hide_transcript', {current_time: this.getCurrentTime()}); + }, + + onShowCaptions: function() { + this.log('edx.video.closed_captions.shown', {current_time: this.getCurrentTime()}); + }, + + onHideCaptions: function() { + this.log('edx.video.closed_captions.hidden', {current_time: this.getCurrentTime()}); + }, + + getCurrentTime: function() { + var player = this.state.videoPlayer; + return player ? player.currentTime : 0; + }, + + getCurrentLanguage: function() { + var language = this.state.lang; + return language; + }, + + log: function(eventName, data) { + var logInfo = _.extend({ + id: this.state.id, + code: this.state.isYoutubeType() ? this.state.youtubeId() : 'html5' + }, data, this.options.data); + Logger.log(eventName, logInfo); } - }, + }; - onPause: function () { - this.log('pause_video', {currentTime: this.getCurrentTime()}); - this.emitPlayVideoEvent = true; - }, - - onEnded: function () { - this.log('stop_video', {currentTime: this.getCurrentTime()}); - this.emitPlayVideoEvent = true; - }, - - onSkip: function (event, doNotShowAgain) { - var info = {currentTime: this.getCurrentTime()}, - eventName = doNotShowAgain ? 'do_not_show_again_video': 'skip_video'; - this.log(eventName, info); - }, - - onSeek: function (event, time, oldTime, type) { - this.log('seek_video', { - old_time: oldTime, - new_time: time, - type: type - }); - }, - - onSpeedChange: function (event, newSpeed, oldSpeed) { - this.log('speed_change_video', { - current_time: this.getCurrentTime(), - old_speed: oldSpeed, - new_speed: newSpeed - }); - }, - - onShowLanguageMenu: function () { - this.log('edx.video.language_menu.shown'); - }, - - onHideLanguageMenu: function () { - this.log('edx.video.language_menu.hidden', { language: this.getCurrentLanguage() }); - }, - - onShowTranscript: function () { - this.log('show_transcript', {current_time: this.getCurrentTime()}); - }, - - onHideTranscript: function () { - this.log('hide_transcript', {current_time: this.getCurrentTime()}); - }, - - onShowCaptions: function () { - this.log('edx.video.closed_captions.shown', {current_time: this.getCurrentTime()}); - }, - - onHideCaptions: function () { - this.log('edx.video.closed_captions.hidden', {current_time: this.getCurrentTime()}); - }, - - getCurrentTime: function () { - var player = this.state.videoPlayer; - return player ? player.currentTime : 0; - }, - - getCurrentLanguage: function() { - var language = this.state.lang; - return language; - }, - - log: function (eventName, data) { - var logInfo = _.extend({ - id: this.state.id, - code: this.state.isYoutubeType() ? this.state.youtubeId() : 'html5' - }, data, this.options.data); - Logger.log(eventName, logInfo); - } - }; - - return EventsPlugin; -}); + return EventsPlugin; + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_play_pause_control.js b/common/lib/xmodule/xmodule/js/src/video/09_play_pause_control.js index 8f556c1469..85d6e1870a 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_play_pause_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_play_pause_control.js @@ -1,6 +1,6 @@ (function(define) { -'use strict'; -define('video/09_play_pause_control.js', [], function() { + 'use strict'; + define('video/09_play_pause_control.js', [], function() { /** * Play/pause control module. * @exports video/09_play_pause_control.js @@ -9,88 +9,88 @@ define('video/09_play_pause_control.js', [], function() { * @param {Object} i18n The object containing strings with translations. * @return {jquery Promise} */ - var PlayPauseControl = function(state, i18n) { - if (!(this instanceof PlayPauseControl)) { - return new PlayPauseControl(state, i18n); - } + var PlayPauseControl = function(state, i18n) { + if (!(this instanceof PlayPauseControl)) { + return new PlayPauseControl(state, i18n); + } - _.bindAll(this, 'play', 'pause', 'onClick', 'destroy'); - this.state = state; - this.state.videoPlayPauseControl = this; - this.i18n = i18n; - this.initialize(); + _.bindAll(this, 'play', 'pause', 'onClick', 'destroy'); + this.state = state; + this.state.videoPlayPauseControl = this; + this.i18n = i18n; + this.initialize(); - return $.Deferred().resolve().promise(); - }; + return $.Deferred().resolve().promise(); + }; - PlayPauseControl.prototype = { - template: [ - '' - ].join(''), + '' + ].join(''), - destroy: function () { - this.el.remove(); - this.state.el.off('destroy', this.destroy); - delete this.state.videoPlayPauseControl; - }, + destroy: function() { + this.el.remove(); + this.state.el.off('destroy', this.destroy); + delete this.state.videoPlayPauseControl; + }, /** Initializes the module. */ - initialize: function() { - this.el = $(this.template); - this.render(); - this.bindHandlers(); - }, + initialize: function() { + this.el = $(this.template); + this.render(); + this.bindHandlers(); + }, /** * Creates any necessary DOM elements, attach them, and set their, * initial configuration. */ - render: function() { - this.state.el.find('.vcr').prepend(this.el); - }, + render: function() { + this.state.el.find('.vcr').prepend(this.el); + }, /** Bind any necessary function callbacks to DOM events. */ - bindHandlers: function() { - this.el.on({ - 'click': this.onClick - }); - this.state.el.on({ - 'play': this.play, - 'pause ended': this.pause, - 'destroy': this.destroy - }); - }, + bindHandlers: function() { + this.el.on({ + 'click': this.onClick + }); + this.state.el.on({ + 'play': this.play, + 'pause ended': this.pause, + 'destroy': this.destroy + }); + }, - onClick: function (event) { - event.preventDefault(); - this.state.videoCommands.execute('togglePlayback'); - }, + onClick: function(event) { + event.preventDefault(); + this.state.videoCommands.execute('togglePlayback'); + }, - play: function () { - this.el + play: function() { + this.el .addClass('pause') .removeClass('play') .attr('title', gettext('Pause')) .find('.icon') .removeClass('fa-play') .addClass('fa-pause'); - }, + }, - pause: function () { - this.el + pause: function() { + this.el .removeClass('pause') .addClass('play') .attr('title', gettext('Play')) .find('.icon') .removeClass('fa-pause') .addClass('fa-play'); - } - }; + } + }; - return PlayPauseControl; -}); + return PlayPauseControl; + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_play_placeholder.js b/common/lib/xmodule/xmodule/js/src/video/09_play_placeholder.js index bcd20dabbf..053068e95e 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_play_placeholder.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_play_placeholder.js @@ -1,6 +1,6 @@ (function(define) { -'use strict'; -define('video/09_play_placeholder.js', [], function() { + 'use strict'; + define('video/09_play_placeholder.js', [], function() { /** * Play placeholder control module. * @exports video/09_play_placeholder.js @@ -9,79 +9,79 @@ define('video/09_play_placeholder.js', [], function() { * @param {Object} i18n The object containing strings with translations. * @return {jquery Promise} */ - var PlayPlaceholder = function(state, i18n) { - if (!(this instanceof PlayPlaceholder)) { - return new PlayPlaceholder(state, i18n); - } + var PlayPlaceholder = function(state, i18n) { + if (!(this instanceof PlayPlaceholder)) { + return new PlayPlaceholder(state, i18n); + } - _.bindAll(this, 'onClick', 'hide', 'show', 'destroy'); - this.state = state; - this.state.videoPlayPlaceholder = this; - this.i18n = i18n; - this.initialize(); + _.bindAll(this, 'onClick', 'hide', 'show', 'destroy'); + this.state = state; + this.state.videoPlayPlaceholder = this; + this.i18n = i18n; + this.initialize(); - return $.Deferred().resolve().promise(); - }; + return $.Deferred().resolve().promise(); + }; - PlayPlaceholder.prototype = { - destroy: function () { - this.el.off('click', this.onClick); - this.state.el.on({ - 'destroy': this.destroy, - 'play': this.hide, - 'ended pause': this.show - }); - this.hide(); - delete this.state.videoPlayPlaceholder; - }, + PlayPlaceholder.prototype = { + destroy: function() { + this.el.off('click', this.onClick); + this.state.el.on({ + 'destroy': this.destroy, + 'play': this.hide, + 'ended pause': this.show + }); + this.hide(); + delete this.state.videoPlayPlaceholder; + }, /** * Indicates whether the placeholder should be shown. We display it * for html5 videos on iPad and Android devices. * @return {Boolean} */ - shouldBeShown: function () { - return /iPad|Android/i.test(this.state.isTouch[0]) && !this.state.isYoutubeType(); - }, + shouldBeShown: function() { + return /iPad|Android/i.test(this.state.isTouch[0]) && !this.state.isYoutubeType(); + }, /** Initializes the module. */ - initialize: function() { - if (!this.shouldBeShown()) { - return false; - } + initialize: function() { + if (!this.shouldBeShown()) { + return false; + } - this.el = this.state.el.find('.btn-play'); - this.bindHandlers(); - this.show(); - }, + this.el = this.state.el.find('.btn-play'); + this.bindHandlers(); + this.show(); + }, /** Bind any necessary function callbacks to DOM events. */ - bindHandlers: function() { - this.el.on('click', this.onClick); - this.state.el.on({ - 'destroy': this.destroy, - 'play': this.hide, - 'ended pause': this.show - }); - }, + bindHandlers: function() { + this.el.on('click', this.onClick); + this.state.el.on({ + 'destroy': this.destroy, + 'play': this.hide, + 'ended pause': this.show + }); + }, - onClick: function () { - this.state.videoCommands.execute('play'); - }, + onClick: function() { + this.state.videoCommands.execute('play'); + }, - hide: function () { - this.el + hide: function() { + this.el .addClass('is-hidden') .attr({'aria-hidden': 'true', 'tabindex': -1}); - }, + }, - show: function () { - this.el + show: function() { + this.el .removeClass('is-hidden') .attr({'aria-hidden': 'false', 'tabindex': 0}); - } - }; + } + }; - return PlayPlaceholder; -}); + return PlayPlaceholder; + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_play_skip_control.js b/common/lib/xmodule/xmodule/js/src/video/09_play_skip_control.js index c2ea39278e..003167ef0b 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_play_skip_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_play_skip_control.js @@ -1,6 +1,6 @@ (function(define) { -'use strict'; -define('video/09_play_skip_control.js', [], function() { + 'use strict'; + define('video/09_play_skip_control.js', [], function() { /** * Play/skip control module. * @exports video/09_play_skip_control.js @@ -9,70 +9,70 @@ define('video/09_play_skip_control.js', [], function() { * @param {Object} i18n The object containing strings with translations. * @return {jquery Promise} */ - var PlaySkipControl = function(state, i18n) { - if (!(this instanceof PlaySkipControl)) { - return new PlaySkipControl(state, i18n); - } + var PlaySkipControl = function(state, i18n) { + if (!(this instanceof PlaySkipControl)) { + return new PlaySkipControl(state, i18n); + } - _.bindAll(this, 'play', 'onClick', 'destroy'); - this.state = state; - this.state.videoPlaySkipControl = this; - this.i18n = i18n; - this.initialize(); + _.bindAll(this, 'play', 'onClick', 'destroy'); + this.state = state; + this.state.videoPlaySkipControl = this; + this.i18n = i18n; + this.initialize(); - return $.Deferred().resolve().promise(); - }; + return $.Deferred().resolve().promise(); + }; - PlaySkipControl.prototype = { - template: [ - '' - ].join(''), + '' + ].join(''), - destroy: function () { - this.el.remove(); - this.state.el.off('destroy', this.destroy); - delete this.state.videoPlaySkipControl; - }, + destroy: function() { + this.el.remove(); + this.state.el.off('destroy', this.destroy); + delete this.state.videoPlaySkipControl; + }, /** Initializes the module. */ - initialize: function() { - this.el = $(this.template); - this.render(); - this.bindHandlers(); - }, + initialize: function() { + this.el = $(this.template); + this.render(); + this.bindHandlers(); + }, /** * Creates any necessary DOM elements, attach them, and set their, * initial configuration. */ - render: function() { - this.state.el.find('.vcr').prepend(this.el); - }, + render: function() { + this.state.el.find('.vcr').prepend(this.el); + }, /** Bind any necessary function callbacks to DOM events. */ - bindHandlers: function() { - this.el.on('click', this.onClick); - this.state.el.on({ - 'play': this.play, - 'destroy': this.destroy - }); - }, + bindHandlers: function() { + this.el.on('click', this.onClick); + this.state.el.on({ + 'play': this.play, + 'destroy': this.destroy + }); + }, - onClick: function (event) { - event.preventDefault(); - if (this.state.videoPlayer.isPlaying()) { - this.state.videoCommands.execute('skip'); - } else { - this.state.videoCommands.execute('play'); - } - }, + onClick: function(event) { + event.preventDefault(); + if (this.state.videoPlayer.isPlaying()) { + this.state.videoCommands.execute('skip'); + } else { + this.state.videoCommands.execute('play'); + } + }, - play: function () { - this.el + play: function() { + this.el .removeClass('play') .addClass('skip') .attr('title', gettext('Skip')) @@ -80,10 +80,10 @@ define('video/09_play_skip_control.js', [], function() { .removeClass('fa-play') .addClass('fa-step-forward'); // Disable possibility to pause the video. - this.state.el.find('video').off('click'); - } - }; + this.state.el.find('video').off('click'); + } + }; - return PlaySkipControl; -}); + return PlaySkipControl; + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_poster.js b/common/lib/xmodule/xmodule/js/src/video/09_poster.js index ba5da94c77..9d74bdfab6 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_poster.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_poster.js @@ -1,6 +1,6 @@ -(function (define) { -'use strict'; -define('video/09_poster.js', [], function () { +(function(define) { + 'use strict'; + define('video/09_poster.js', [], function() { /** * Poster module. * @exports video/09_poster.js @@ -8,62 +8,62 @@ define('video/09_poster.js', [], function () { * @param {jquery Element} element * @param {Object} options */ - var VideoPoster = function (element, options) { - if (!(this instanceof VideoPoster)) { - return new VideoPoster(element, options); - } + var VideoPoster = function(element, options) { + if (!(this instanceof VideoPoster)) { + return new VideoPoster(element, options); + } - _.bindAll(this, 'onClick', 'destroy'); - this.element = element; - this.container = element.find('.video-player'); - this.options = options || {}; - this.initialize(); - }; + _.bindAll(this, 'onClick', 'destroy'); + this.element = element; + this.container = element.find('.video-player'); + this.options = options || {}; + this.initialize(); + }; - VideoPoster.moduleName = 'Poster'; - VideoPoster.prototype = { - template: _.template([ - '
      ', '', - '
      ' - ].join('')), + '
    2. ' + ].join('')), - initialize: function () { - this.el = $(this.template({ - url: this.options.poster.url, - type: this.options.poster.type - })); - this.element.addClass('is-pre-roll'); - this.render(); - this.bindHandlers(); - }, + initialize: function() { + this.el = $(this.template({ + url: this.options.poster.url, + type: this.options.poster.type + })); + this.element.addClass('is-pre-roll'); + this.render(); + this.bindHandlers(); + }, - bindHandlers: function () { - this.el.on('click', this.onClick); - this.element.on('destroy', this.destroy); - }, + bindHandlers: function() { + this.el.on('click', this.onClick); + this.element.on('destroy', this.destroy); + }, - render: function () { - this.container.append(this.el); - }, + render: function() { + this.container.append(this.el); + }, - onClick: function () { - if (_.isFunction(this.options.onClick)) { - this.options.onClick(); + onClick: function() { + if (_.isFunction(this.options.onClick)) { + this.options.onClick(); + } + this.destroy(); + }, + + destroy: function() { + this.element.off('destroy', this.destroy).removeClass('is-pre-roll'); + this.el.remove(); } - this.destroy(); - }, + }; - destroy: function () { - this.element.off('destroy', this.destroy).removeClass('is-pre-roll'); - this.el.remove(); - } - }; - - return VideoPoster; -}); + return VideoPoster; + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js b/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js index df64d9c088..f817c7eb85 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js @@ -1,6 +1,6 @@ (function(define) { -'use strict'; -define('video/09_save_state_plugin.js', [], function() { + 'use strict'; + define('video/09_save_state_plugin.js', [], function() { /** * Save state module. * @exports video/09_save_state_plugin.js @@ -10,114 +10,114 @@ define('video/09_save_state_plugin.js', [], function() { * @param {Object} options * @return {jquery Promise} */ - var SaveStatePlugin = function(state, i18n, options) { - if (!(this instanceof SaveStatePlugin)) { - return new SaveStatePlugin(state, i18n, options); - } - - _.bindAll(this, 'onSpeedChange', 'saveStateHandler', 'bindUnloadHandler', 'onUnload', 'onYoutubeAvailability', - 'onLanguageChange', 'destroy'); - this.state = state; - this.options = _.extend({events: []}, options); - this.state.videoSaveStatePlugin = this; - this.i18n = i18n; - this.initialize(); - - return $.Deferred().resolve().promise(); - }; - - - SaveStatePlugin.moduleName = 'SaveStatePlugin'; - SaveStatePlugin.prototype = { - destroy: function () { - this.state.el.off(this.events).off('destroy', this.destroy); - $(window).off('unload', this.onUnload); - delete this.state.videoSaveStatePlugin; - }, - - initialize: function() { - this.events = { - 'speedchange': this.onSpeedChange, - 'play': this.bindUnloadHandler, - 'pause destroy': this.saveStateHandler, - 'language_menu:change': this.onLanguageChange, - 'youtube_availability': this.onYoutubeAvailability - }; - this.bindHandlers(); - }, - - bindHandlers: function() { - if (this.options.events.length) { - _.each(this.options.events, function (eventName) { - var callback; - if (_.has(this.events, eventName)) { - callback = this.events[eventName]; - this.state.el.on(eventName, callback); - } - }, this); - } else { - this.state.el.on(this.events); + var SaveStatePlugin = function(state, i18n, options) { + if (!(this instanceof SaveStatePlugin)) { + return new SaveStatePlugin(state, i18n, options); } - this.state.el.on('destroy', this.destroy); - }, - bindUnloadHandler: _.once(function () { - $(window).on('unload.video', this.onUnload); - }), + _.bindAll(this, 'onSpeedChange', 'saveStateHandler', 'bindUnloadHandler', 'onUnload', 'onYoutubeAvailability', + 'onLanguageChange', 'destroy'); + this.state = state; + this.options = _.extend({events: []}, options); + this.state.videoSaveStatePlugin = this; + this.i18n = i18n; + this.initialize(); - onSpeedChange: function (event, newSpeed) { - this.saveState(true, {speed: newSpeed}); - this.state.storage.setItem('speed', newSpeed, true); - this.state.storage.setItem('general_speed', newSpeed); - }, + return $.Deferred().resolve().promise(); + }; - saveStateHandler: function () { - this.saveState(true); - }, - onUnload: function () { - this.saveState(); - }, + SaveStatePlugin.moduleName = 'SaveStatePlugin'; + SaveStatePlugin.prototype = { + destroy: function() { + this.state.el.off(this.events).off('destroy', this.destroy); + $(window).off('unload', this.onUnload); + delete this.state.videoSaveStatePlugin; + }, - onLanguageChange: function (event, langCode) { - this.state.storage.setItem('language', langCode); - }, + initialize: function() { + this.events = { + 'speedchange': this.onSpeedChange, + 'play': this.bindUnloadHandler, + 'pause destroy': this.saveStateHandler, + 'language_menu:change': this.onLanguageChange, + 'youtube_availability': this.onYoutubeAvailability + }; + this.bindHandlers(); + }, - onYoutubeAvailability: function (event, youtubeIsAvailable) { + bindHandlers: function() { + if (this.options.events.length) { + _.each(this.options.events, function(eventName) { + var callback; + if (_.has(this.events, eventName)) { + callback = this.events[eventName]; + this.state.el.on(eventName, callback); + } + }, this); + } else { + this.state.el.on(this.events); + } + this.state.el.on('destroy', this.destroy); + }, + + bindUnloadHandler: _.once(function() { + $(window).on('unload.video', this.onUnload); + }), + + onSpeedChange: function(event, newSpeed) { + this.saveState(true, {speed: newSpeed}); + this.state.storage.setItem('speed', newSpeed, true); + this.state.storage.setItem('general_speed', newSpeed); + }, + + saveStateHandler: function() { + this.saveState(true); + }, + + onUnload: function() { + this.saveState(); + }, + + onLanguageChange: function(event, langCode) { + this.state.storage.setItem('language', langCode); + }, + + onYoutubeAvailability: function(event, youtubeIsAvailable) { // Compare what the client-side code has determined Youtube // availability to be (true/false) vs. what the LMS recorded for // this user. The LMS will assume YouTube is available by default. - if (youtubeIsAvailable !== this.state.config.recordedYoutubeIsAvailable) { - this.saveState(true, {youtube_is_available: youtubeIsAvailable}); + if (youtubeIsAvailable !== this.state.config.recordedYoutubeIsAvailable) { + this.saveState(true, {youtube_is_available: youtubeIsAvailable}); + } + }, + + saveState: function(async, data) { + if (!($.isPlainObject(data))) { + data = { + saved_video_position: this.state.videoPlayer.currentTime + }; + } + + if (data.speed) { + this.state.storage.setItem('speed', data.speed, true); + } + + if (_.has(data, 'saved_video_position')) { + this.state.storage.setItem('savedVideoPosition', data.saved_video_position, true); + data.saved_video_position = Time.formatFull(data.saved_video_position); + } + + $.ajax({ + url: this.state.config.saveStateUrl, + type: 'POST', + async: async ? true : false, + dataType: 'json', + data: data + }); } - }, + }; - saveState: function (async, data) { - if (!($.isPlainObject(data))) { - data = { - saved_video_position: this.state.videoPlayer.currentTime - }; - } - - if (data.speed) { - this.state.storage.setItem('speed', data.speed, true); - } - - if (_.has(data, 'saved_video_position')) { - this.state.storage.setItem('savedVideoPosition', data.saved_video_position, true); - data.saved_video_position = Time.formatFull(data.saved_video_position); - } - - $.ajax({ - url: this.state.config.saveStateUrl, - type: 'POST', - async: async ? true : false, - dataType: 'json', - data: data - }); - } - }; - - return SaveStatePlugin; -}); + return SaveStatePlugin; + }); }(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/js/src/video/09_skip_control.js b/common/lib/xmodule/xmodule/js/src/video/09_skip_control.js index 4f09bc3214..40a4c908b3 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_skip_control.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_skip_control.js @@ -1,7 +1,7 @@ (function(define) { -'use strict'; + 'use strict'; // VideoSkipControl module. -define( + define( 'video/09_skip_control.js', [], function() { /** @@ -29,13 +29,13 @@ function() { SkipControl.prototype = { template: [ '' ].join(''), - destroy: function () { + destroy: function() { this.el.remove(); this.state.el.off('.skip'); delete this.state.videoSkipControl; @@ -64,7 +64,7 @@ function() { }); }, - onClick: function (event) { + onClick: function(event) { event.preventDefault(); this.state.videoCommands.execute('skip', true); } diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js index 68337c05ed..b784ae6702 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js @@ -2,13 +2,12 @@ // VideoCaption module. 'use strict'; - define('video/09_video_caption.js',[ + define('video/09_video_caption.js', [ 'video/00_sjson.js', 'video/00_async_process.js', 'edx-ui-toolkit/js/utils/html-utils', 'draggabilly' - ], function (Sjson, AsyncProcess, HtmlUtils, Draggabilly) { - + ], function(Sjson, AsyncProcess, HtmlUtils, Draggabilly) { /** * @desc VideoCaption module exports a function. * @@ -23,7 +22,7 @@ * * @returns {jquery Promise} */ - var VideoCaption = function (state) { + var VideoCaption = function(state) { if (!(this instanceof VideoCaption)) { return new VideoCaption(state); } @@ -48,7 +47,7 @@ VideoCaption.prototype = { - destroy: function () { + destroy: function() { this.state.el .off({ 'caption:fetch': this.fetchCaption, @@ -75,29 +74,29 @@ * @desc Initiate rendering of elements, and set their initial configuration. * */ - renderElements: function () { + renderElements: function() { var languages = this.state.config.transcriptLanguages; var langHtml = HtmlUtils.interpolateHtml( HtmlUtils.HTML( - [ - '
      ', - '', - '', - '', - '
      ' - ].join(''), + [ + '
      ', + '', + '', + '', + '
      ' + ].join(''), { langTitle: gettext('Open language menu') } @@ -109,8 +108,8 @@ HtmlUtils.HTML( [ '
      ', - '

      ', - '
        ', + '

        ', + '
          ', '
          ' ].join('')), { @@ -140,7 +139,7 @@ * mousemove, etc.). * */ - bindHandlers: function () { + bindHandlers: function() { var state = this.state, events = [ 'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur', @@ -195,7 +194,7 @@ } }, - onCaptionUpdate: function (event, time) { + onCaptionUpdate: function(event, time) { this.updatePlayTime(time); }, @@ -203,11 +202,11 @@ var KEY = $.ui.keyCode, keyCode = event.keyCode; - switch(keyCode) { - case KEY.SPACE: - case KEY.ENTER: - event.preventDefault(); - this.toggleClosedCaptions(event); + switch (keyCode) { + case KEY.SPACE: + case KEY.ENTER: + event.preventDefault(); + this.toggleClosedCaptions(event); } }, @@ -215,11 +214,11 @@ var KEY = $.ui.keyCode, keyCode = event.keyCode; - switch(keyCode) { - case KEY.SPACE: - case KEY.ENTER: - event.preventDefault(); - this.toggle(event); + switch (keyCode) { + case KEY.SPACE: + case KEY.ENTER: + event.preventDefault(); + this.toggle(event); } }, @@ -228,32 +227,32 @@ keyCode = event.keyCode, focused, index, total; - switch(keyCode) { - case KEY.UP: - event.preventDefault(); - focused = $(':focus').parent(); - index = this.languageChooserEl.find('li').index(focused); - total = this.languageChooserEl.find('li').size() - 1; + switch (keyCode) { + case KEY.UP: + event.preventDefault(); + focused = $(':focus').parent(); + index = this.languageChooserEl.find('li').index(focused); + total = this.languageChooserEl.find('li').size() - 1; - this.previousLanguageMenuItem(event, index); - break; + this.previousLanguageMenuItem(event, index); + break; - case KEY.DOWN: - event.preventDefault(); - focused = $(':focus').parent(); - index = this.languageChooserEl.find('li').index(focused); - total = this.languageChooserEl.find('li').size() - 1; + case KEY.DOWN: + event.preventDefault(); + focused = $(':focus').parent(); + index = this.languageChooserEl.find('li').index(focused); + total = this.languageChooserEl.find('li').size() - 1; - this.nextLanguageMenuItem(event, index, total); - break; + this.nextLanguageMenuItem(event, index, total); + break; - case KEY.ESCAPE: - this.closeLanguageMenu(event); - break; + case KEY.ESCAPE: + this.closeLanguageMenu(event); + break; - case KEY.ENTER: - case KEY.SPACE: - return true; + case KEY.ENTER: + case KEY.SPACE: + return true; } }, @@ -261,18 +260,18 @@ var KEY = $.ui.keyCode, keyCode = event.keyCode; - switch(keyCode) { + switch (keyCode) { // Handle keypresses - case KEY.ENTER: - case KEY.SPACE: - case KEY.UP: - event.preventDefault(); - this.openLanguageMenu(event); - break; + case KEY.ENTER: + case KEY.SPACE: + case KEY.UP: + event.preventDefault(); + this.openLanguageMenu(event); + break; - case KEY.ESCAPE: - this.closeLanguageMenu(event); - break; + case KEY.ESCAPE: + this.closeLanguageMenu(event); + break; } return event.keyCode === KEY.TAB; @@ -347,27 +346,27 @@ .focus(); }, - onCaptionHandler: function (event) { + onCaptionHandler: function(event) { switch (event.type) { - case 'mouseover': - case 'mouseout': - this.captionMouseOverOut(event); - break; - case 'mousedown': - this.captionMouseDown(event); - break; - case 'click': - this.captionClick(event); - break; - case 'focusin': - this.captionFocus(event); - break; - case 'focusout': - this.captionBlur(event); - break; - case 'keydown': - this.captionKeyDown(event); - break; + case 'mouseover': + case 'mouseout': + this.captionMouseOverOut(event); + break; + case 'mousedown': + this.captionMouseDown(event); + break; + case 'click': + this.captionClick(event); + break; + case 'focusin': + this.captionFocus(event); + break; + case 'focusout': + this.captionBlur(event); + break; + case 'keydown': + this.captionKeyDown(event); + break; } }, @@ -376,7 +375,7 @@ * * @param {jquery Event} event */ - onContainerMouseEnter: function (event) { + onContainerMouseEnter: function(event) { event.preventDefault(); $(event.currentTarget).find('.lang').addClass('is-opened'); @@ -393,7 +392,7 @@ * * @param {jquery Event} event */ - onContainerMouseLeave: function (event) { + onContainerMouseLeave: function(event) { event.preventDefault(); $(event.currentTarget).find('.lang').removeClass('is-opened'); @@ -410,7 +409,7 @@ * * @param {jquery Event} event */ - onMouseEnter: function () { + onMouseEnter: function() { if (this.frozen) { clearTimeout(this.frozen); } @@ -426,7 +425,7 @@ * * @param {jquery Event} event */ - onMouseLeave: function () { + onMouseLeave: function() { if (this.frozen) { clearTimeout(this.frozen); } @@ -443,7 +442,7 @@ * * @param {jquery Event} event */ - onMovement: function () { + onMovement: function() { this.onMouseEnter(); }, @@ -452,7 +451,7 @@ * * @returns {array} if [startTime, endTime] are defined */ - getStartEndTimes: function () { + getStartEndTimes: function() { // due to the way config.startTime/endTime are // processed in 03_video_player.js, we assume // endTime can be an integer or null, @@ -469,7 +468,7 @@ * @returns {object} {start, captions} parallel arrays of * start times and corresponding captions */ - getBoundedCaptions: function () { + getBoundedCaptions: function() { // get start and caption. If startTime and endTime // are specified, filter by that range. var times = this.getStartEndTimes(); @@ -478,8 +477,8 @@ var captions = results.captions; return { - 'start': start, - 'captions': captions + 'start': start, + 'captions': captions }; }, @@ -495,7 +494,7 @@ * false: No caption file was specified, or an empty string was * specified for the Youtube type player. */ - fetchCaption: function (fetchWithYoutubeId) { + fetchCaption: function(fetchWithYoutubeId) { var self = this, state = this.state, language = state.getCurrentLanguage(), @@ -531,7 +530,7 @@ url: url, notifyOnError: false, data: data, - success: function (sjson) { + success: function(sjson) { self.sjson = new Sjson(sjson); var results = self.getBoundedCaptions(); var start = results.start; @@ -569,7 +568,7 @@ self.loaded = true; }, - error: function (jqXHR, textStatus, errorThrown) { + error: function(jqXHR, textStatus, errorThrown) { console.log('[Video info]: ERROR while fetching captions.'); console.log( '[Video info]: STATUS:', textStatus + @@ -603,14 +602,14 @@ * * @returns {jquery Promise} */ - fetchAvailableTranslations: function () { + fetchAvailableTranslations: function() { var self = this, state = this.state; this.availableTranslationsXHR = $.ajaxWithPrefix({ url: state.config.transcriptAvailableTranslationsUrl, notifyOnError: false, - success: function (response) { + success: function(response) { var currentLanguages = state.config.transcriptLanguages, newLanguages = _.pick(currentLanguages, response); @@ -625,7 +624,7 @@ self.renderLanguageMenu(newLanguages); } }, - error: function () { + error: function() { self.hideCaptions(true, false); self.state.el.find('.lang').hide(); self.state.el.find('.transcript-control').hide(); @@ -640,7 +639,7 @@ * @desc Recalculates and updates the height of the container of captions. * */ - onResize: function () { + onResize: function() { this.subtitlesEl .find('.spacing').first() .height(this.topSpacingHeight()); @@ -661,7 +660,7 @@ * value - language label * */ - renderLanguageMenu: function (languages) { + renderLanguageMenu: function(languages) { var self = this, state = this.state, $menu = $('
        ') ); var $animElem = $(animElem.toString()); element.after($animElem); @@ -157,31 +156,31 @@ // Prevents "text" is undefined in underscore.js in tests - looks like some tests use // discussions somehow, but never append discussion fixtures or reset them; this causes // entire test suite (lms, cms, common) to fail due to unhandled JS exception - var popupTemplate = $("#alert-popup").html() || ""; - if ($("#discussion-alert").length === 0) { + var popupTemplate = $('#alert-popup').html() || ''; + if ($('#discussion-alert').length === 0) { $alertDiv = $( edx.HtmlUtils.template(popupTemplate)({}).toString() ); - this.makeFocusTrap($alertDiv.find("button")); - $alertTrigger = $("").css("display", "none"); + this.makeFocusTrap($alertDiv.find('button')); + $alertTrigger = $("").css('display', 'none'); $alertTrigger.leanModal({ - closeButton: "#discussion-alert .dismiss", + closeButton: '#discussion-alert .dismiss', overlay: 1, top: 200 }); - $("body").append($alertDiv).append($alertTrigger); + $('body').append($alertDiv).append($alertTrigger); } - $("#discussion-alert header h2").text(header); - $("#discussion-alert p").text(body); - $("#discussion-alert-trigger").click(); - $("#discussion-alert button").focus(); + $('#discussion-alert header h2').text(header); + $('#discussion-alert p').text(body); + $('#discussion-alert-trigger').click(); + $('#discussion-alert button').focus(); }; DiscussionUtil.safeAjax = function(params) { var $elem, deferred, request, self = this; $elem = params.$elem; - if ($elem && $elem.prop("disabled")) { + if ($elem && $elem.prop('disabled')) { deferred = $.Deferred(); deferred.reject(); return deferred.promise(); @@ -192,16 +191,16 @@ if (!params.error) { params.error = function() { self.discussionAlert( - gettext("Sorry"), + gettext('Sorry'), gettext( - "We had some trouble processing your request. Please ensure you have copied any " + - "unsaved work and then reload the page.") + 'We had some trouble processing your request. Please ensure you have copied any ' + + 'unsaved work and then reload the page.') ); }; } if ($elem) { - $elem.prop("disabled", true); + $elem.prop('disabled', true); } if (params.$loading) { if (params.loadingCallback) { @@ -213,7 +212,7 @@ request = $.ajax(params).always(function() { if ($elem) { - $elem.prop("disabled", false); + $elem.prop('disabled', false); } if (params.$loading) { if (params.loadedCallback) { @@ -231,7 +230,7 @@ self = this; if (errorMsg) { safeAjaxParams.error = function() { - return self.discussionAlert(gettext("Sorry"), errorMsg); + return self.discussionAlert(gettext('Sorry'), errorMsg); }; } undo = _.pick(model.attributes, _.keys(updates)); @@ -248,7 +247,7 @@ var event, eventSelector, handler, selector, _ref, _results; _results = []; for (eventSelector in eventsHandler) { - if (eventsHandler.hasOwnProperty(eventSelector)){ + if (eventsHandler.hasOwnProperty(eventSelector)) { handler = eventsHandler[eventSelector]; _ref = eventSelector.split(' '); event = _ref[0]; @@ -264,7 +263,7 @@ var makeErrorElem, response, _i, _len, _ref, _results, $errorItem; makeErrorElem = function(message) { return edx.HtmlUtils.setHtml( - $("
      1. ").addClass("post-error"), + $('
      2. ').addClass('post-error'), message ); }; @@ -283,7 +282,7 @@ } } else { $errorItem = makeErrorElem( - gettext("We had some trouble processing your request. Please try again.") + gettext('We had some trouble processing your request. Please try again.') ); return errorsField.append($errorItem); } @@ -301,11 +300,11 @@ return this.processEachMathAndCode(htmlSnippet, function(s, type) { if (type === 'display') { return s.replace(RE_DISPLAYMATH, function($0, $1) { - return "\\[" + $1 + "\\]"; + return '\\[' + $1 + '\\]'; }); } else if (type === 'inline') { return s.replace(RE_INLINEMATH, function($0, $1) { - return "\\(" + $1 + "\\)"; + return '\\(' + $1 + '\\)'; }); } else { return s; @@ -315,10 +314,10 @@ DiscussionUtil.makeWmdEditor = function($content, $local, cls_identifier) { var appended_id, editor, elem, id, imageUploadUrl, placeholder, _processor; - elem = $local("." + cls_identifier); + elem = $local('.' + cls_identifier); placeholder = elem.data('placeholder'); - id = elem.attr("data-id"); - appended_id = "-" + cls_identifier + "-" + id; + id = elem.attr('data-id'); + appended_id = '-' + cls_identifier + '-' + id; imageUploadUrl = this.urlFor('upload'); _processor = function(self) { return function(text) { @@ -326,25 +325,25 @@ }; }; editor = Markdown.makeWmdEditor(elem, appended_id, imageUploadUrl, _processor(this)); - this.wmdEditors["" + cls_identifier + "-" + id] = editor; + this.wmdEditors['' + cls_identifier + '-' + id] = editor; if (placeholder) { - elem.find("#wmd-input" + appended_id).attr('placeholder', placeholder); + elem.find('#wmd-input' + appended_id).attr('placeholder', placeholder); } return editor; }; DiscussionUtil.getWmdEditor = function($content, $local, cls_identifier) { var elem, id; - elem = $local("." + cls_identifier); - id = elem.attr("data-id"); - return this.wmdEditors["" + cls_identifier + "-" + id]; + elem = $local('.' + cls_identifier); + id = elem.attr('data-id'); + return this.wmdEditors['' + cls_identifier + '-' + id]; }; DiscussionUtil.getWmdInput = function($content, $local, cls_identifier) { var elem, id; - elem = $local("." + cls_identifier); - id = elem.attr("data-id"); - return $local("#wmd-input-" + cls_identifier + "-" + id); + elem = $local('.' + cls_identifier); + id = elem.attr('data-id'); + return $local('#wmd-input-' + cls_identifier + '-' + id); }; DiscussionUtil.getWmdContent = function($content, $local, cls_identifier) { @@ -370,9 +369,9 @@ DiscussionUtil.processEachMathAndCode = function(htmlSnippet, processor) { var $div, codeArchive, processedHtmlString, htmlString; codeArchive = {}; - processedHtmlString = ""; - $div = edx.HtmlUtils.setHtml($("
        "), edx.HtmlUtils.ensureHtml(htmlSnippet)); - $div.find("code").each(function(index, code) { + processedHtmlString = ''; + $div = edx.HtmlUtils.setHtml($('
        '), edx.HtmlUtils.ensureHtml(htmlSnippet)); + $div.find('code').each(function(index, code) { codeArchive[index] = $(code).html(); return $(code).text(index); }); @@ -382,7 +381,7 @@ while (true) { if (RE_INLINEMATH.test(htmlString)) { htmlString = htmlString.replace(RE_INLINEMATH, function($0, $1, $2, $3) { - processedHtmlString += $1 + processor("$" + $2 + "$", 'inline'); + processedHtmlString += $1 + processor('$' + $2 + '$', 'inline'); return $3; }); } else if (RE_DISPLAYMATH.test(htmlString)) { @@ -390,7 +389,7 @@ /* bug fix, ordering is off */ - processedHtmlString = processor("$$" + $2 + "$$", 'display') + processedHtmlString; + processedHtmlString = processor('$$' + $2 + '$$', 'display') + processedHtmlString; processedHtmlString = $1 + processedHtmlString; return $3; }); @@ -404,11 +403,11 @@ htmlString = htmlString.replace(new RegExp(ESCAPED_DOLLAR, 'g'), '\\$'); htmlString = htmlString.replace(/\\\\\\\\/g, ESCAPED_BACKSLASH); htmlString = htmlString.replace(/\\begin\{([a-z]*\*?)\}([\s\S]*?)\\end\{\1\}/img, function($0, $1, $2) { - return processor(("\\begin{" + $1 + "}") + $2 + ("\\end{" + $1 + "}")); + return processor(('\\begin{' + $1 + '}') + $2 + ('\\end{' + $1 + '}')); }); htmlString = htmlString.replace(new RegExp(ESCAPED_BACKSLASH, 'g'), '\\\\\\\\'); - $div = edx.HtmlUtils.setHtml($("
        "), edx.HtmlUtils.HTML(htmlString)); - $div.find("code").each(function(index, code) { + $div = edx.HtmlUtils.setHtml($('
        '), edx.HtmlUtils.HTML(htmlString)); + $div.find('code').each(function(index, code) { edx.HtmlUtils.setHtml( $(code), edx.HtmlUtils.HTML(processor(codeArchive[index], 'code')) @@ -421,15 +420,15 @@ return edx.HtmlUtils.HTML( htmlSnippet.toString().replace( /\<\;highlight\>\;/g, - "").replace(/\<\;\/highlight\>\;/g, "" + "").replace(/\<\;\/highlight\>\;/g, '' ) ); }; DiscussionUtil.stripHighlight = function(htmlString) { return htmlString - .replace(/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, "") - .replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, ""); + .replace(/\&(amp\;)?lt\;highlight\&(amp\;)?gt\;/g, '') + .replace(/\&(amp\;)?lt\;\/highlight\&(amp\;)?gt\;/g, ''); }; DiscussionUtil.stripLatexHighlight = function(htmlSnippet) { @@ -443,7 +442,7 @@ */ DiscussionUtil.markdownWithHighlight = function(unsafeText) { var converter; - unsafeText = unsafeText.replace(/^\>\;/gm, ">"); + unsafeText = unsafeText.replace(/^\>\;/gm, '>'); converter = Markdown.getMathCompatibleConverter(); /* * converter.makeHtml and HTML escaping: @@ -476,8 +475,8 @@ }; DiscussionUtil.typesetMathJax = function(element) { - if (typeof MathJax !== "undefined" && MathJax !== null) { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, element[0]]); + if (typeof MathJax !== 'undefined' && MathJax !== null) { + MathJax.Hub.Queue(['Typeset', MathJax.Hub, element[0]]); } }; @@ -489,23 +488,23 @@ ellipsis: gettext('…') })); $result = $(edx.HtmlUtils.joinHtml( - edx.HtmlUtils.HTML("
        "), + edx.HtmlUtils.HTML('
        '), truncated_text, - edx.HtmlUtils.HTML("
        ") + edx.HtmlUtils.HTML('
        ') ).toString()); - imagesToReplace = $result.find("img:not(:first)"); + imagesToReplace = $result.find('img:not(:first)'); if (imagesToReplace.length > 0) { edx.HtmlUtils.append( $result, edx.HtmlUtils.interpolateHtml( - edx.HtmlUtils.HTML("

        {text}

        "), - {text: gettext("Some images in this post have been omitted")} + edx.HtmlUtils.HTML('

        {text}

        '), + {text: gettext('Some images in this post have been omitted')} ) ); } // See TNL-4983 for an explanation of why the linter requires ensureHtml() var afterMessage = edx.HtmlUtils.interpolateHtml( - edx.HtmlUtils.HTML("{text}"), {text: gettext("image omitted")} + edx.HtmlUtils.HTML('{text}'), {text: gettext('image omitted')} ); imagesToReplace.after(edx.HtmlUtils.ensureHtml(afterMessage).toString()).remove(); return $result.html(); diff --git a/common/static/common/js/discussion/views/discussion_content_view.js b/common/static/common/js/discussion/views/discussion_content_view.js index 55bded5d34..19e965e143 100644 --- a/common/static/common/js/discussion/views/discussion_content_view.js +++ b/common/static/common/js/discussion/views/discussion_content_view.js @@ -18,9 +18,8 @@ return child; }; - if (typeof Backbone !== "undefined" && Backbone !== null) { + if (typeof Backbone !== 'undefined' && Backbone !== null) { this.DiscussionContentView = (function(_super) { - __extends(DiscussionContentView, _super); function DiscussionContentView() { @@ -41,8 +40,8 @@ } DiscussionContentView.prototype.events = { - "click .discussion-flag-abuse": "toggleFlagAbuse", - "keydown .discussion-flag-abuse": function(event) { + 'click .discussion-flag-abuse': 'toggleFlagAbuse', + 'keydown .discussion-flag-abuse': function(event) { return DiscussionUtil.activateOnSpace(event, this.toggleFlagAbuse); } }; @@ -53,7 +52,7 @@ _ref = this.abilityRenderer; _results = []; for (action in _ref) { - if (_ref.hasOwnProperty(action)){ + if (_ref.hasOwnProperty(action)) { selector = _ref[action]; if (!ability[action]) { _results.push(selector.disable.apply(this)); @@ -69,40 +68,40 @@ DiscussionContentView.prototype.abilityRenderer = { editable: { enable: function() { - return this.$(".action-edit").closest(".actions-item").removeClass("is-hidden"); + return this.$('.action-edit').closest('.actions-item').removeClass('is-hidden'); }, disable: function() { - return this.$(".action-edit").closest(".actions-item").addClass("is-hidden"); + return this.$('.action-edit').closest('.actions-item').addClass('is-hidden'); } }, can_delete: { enable: function() { - return this.$(".action-delete").closest(".actions-item").removeClass("is-hidden"); + return this.$('.action-delete').closest('.actions-item').removeClass('is-hidden'); }, disable: function() { - return this.$(".action-delete").closest(".actions-item").addClass("is-hidden"); + return this.$('.action-delete').closest('.actions-item').addClass('is-hidden'); } }, can_openclose: { enable: function() { var self = this; - return _.each([".action-close", ".action-pin"], function(selector) { - return self.$(selector).closest(".actions-item").removeClass("is-hidden"); + return _.each(['.action-close', '.action-pin'], function(selector) { + return self.$(selector).closest('.actions-item').removeClass('is-hidden'); }); }, disable: function() { var self = this; - return _.each([".action-close", ".action-pin"], function(selector) { - return self.$(selector).closest(".actions-item").addClass("is-hidden"); + return _.each(['.action-close', '.action-pin'], function(selector) { + return self.$(selector).closest('.actions-item').addClass('is-hidden'); }); } }, can_report: { enable: function() { - return this.$(".action-report").closest(".actions-item").removeClass("is-hidden"); + return this.$('.action-report').closest('.actions-item').removeClass('is-hidden'); }, disable: function() { - return this.$(".action-report").closest(".actions-item").addClass("is-hidden"); + return this.$('.action-report').closest('.actions-item').addClass('is-hidden'); } }, can_vote: { @@ -150,7 +149,7 @@ }; DiscussionContentView.prototype.makeWmdEditor = function(cls_identifier) { - if (!this.$el.find(".wmd-panel").length) { + if (!this.$el.find('.wmd-panel').length) { return DiscussionUtil.makeWmdEditor(this.$el, $.proxy(this.$, this), cls_identifier); } }; @@ -170,15 +169,14 @@ DiscussionContentView.prototype.initialize = function() { var self = this; this.model.bind('change', this.renderPartialAttrs, this); - return this.listenTo(this.model, "change:endorsed", function() { + return this.listenTo(this.model, 'change:endorsed', function() { if (self.model instanceof Comment) { - return self.trigger("comment:endorse"); + return self.trigger('comment:endorse'); } }); }; return DiscussionContentView; - })(Backbone.View); this.DiscussionContentShowView = (function(_super) { __extends(DiscussionContentShowView, _super); @@ -220,25 +218,25 @@ DiscussionContentShowView.prototype.events = _.reduce( [ - [".action-follow", "toggleFollow"], - [".action-answer", "toggleEndorse"], - [".action-endorse", "toggleEndorse"], - [".action-vote", "toggleVote"], - [".action-more", "toggleSecondaryActions"], - [".action-pin", "togglePin"], - [".action-edit", "edit"], - [".action-delete", "_delete"], - [".action-report", "toggleReport"], - [".action-close", "toggleClose"] + ['.action-follow', 'toggleFollow'], + ['.action-answer', 'toggleEndorse'], + ['.action-endorse', 'toggleEndorse'], + ['.action-vote', 'toggleVote'], + ['.action-more', 'toggleSecondaryActions'], + ['.action-pin', 'togglePin'], + ['.action-edit', 'edit'], + ['.action-delete', '_delete'], + ['.action-report', 'toggleReport'], + ['.action-close', 'toggleClose'] ], function(obj, event) { var funcName, selector; selector = event[0]; funcName = event[1]; - obj["click " + selector] = function(event) { + obj['click ' + selector] = function(event) { return this[funcName](event); }; - obj["keydown " + selector] = function(event) { + obj['keydown ' + selector] = function(event) { return DiscussionUtil.activateOnSpace(event, this[funcName]); }; return obj; @@ -249,8 +247,8 @@ DiscussionContentShowView.prototype.updateButtonState = function(selector, checked) { var $button; $button = this.$(selector); - $button.toggleClass("is-checked", checked); - return $button.attr("aria-checked", checked); + $button.toggleClass('is-checked', checked); + return $button.attr('aria-checked', checked); }; DiscussionContentShowView.prototype.attrRenderer = $.extend( @@ -258,50 +256,50 @@ DiscussionContentView.prototype.attrRenderer, { subscribed: function(subscribed) { - return this.updateButtonState(".action-follow", subscribed); + return this.updateButtonState('.action-follow', subscribed); }, endorsed: function(endorsed) { var $button, selector; - selector = this.model.get("thread").get("thread_type") === "question" ? - ".action-answer" : - ".action-endorse"; + selector = this.model.get('thread').get('thread_type') === 'question' ? + '.action-answer' : + '.action-endorse'; this.updateButtonState(selector, endorsed); $button = this.$(selector); - $button.closest(".actions-item").toggleClass("is-hidden", !this.model.canBeEndorsed()); - return $button.toggleClass("is-checked", endorsed); + $button.closest('.actions-item').toggleClass('is-hidden', !this.model.canBeEndorsed()); + return $button.toggleClass('is-checked', endorsed); }, votes: function(votes) { var button, numVotes, selector, votesText, votesCountMsg; - selector = ".action-vote"; + selector = '.action-vote'; this.updateButtonState(selector, window.user.voted(this.model)); button = this.$el.find(selector); numVotes = votes.up_count; votesCountMsg = ngettext( - "there is currently {numVotes} vote", "there are currently {numVotes} votes", numVotes + 'there is currently {numVotes} vote', 'there are currently {numVotes} votes', numVotes ); - button.find(".js-sr-vote-count").empty().text( - edx.StringUtils.interpolate(votesCountMsg, {numVotes: numVotes }) + button.find('.js-sr-vote-count').empty().text( + edx.StringUtils.interpolate(votesCountMsg, {numVotes: numVotes}) ); votesText = edx.StringUtils.interpolate( - ngettext("{numVotes} Vote", "{numVotes} Votes", numVotes), - { numVotes: numVotes }); - button.find(".vote-count").empty().text(votesText); + ngettext('{numVotes} Vote', '{numVotes} Votes', numVotes), + {numVotes: numVotes}); + button.find('.vote-count').empty().text(votesText); this.$el.find('.display-vote .vote-count').empty().text(votesText); }, pinned: function(pinned) { - this.updateButtonState(".action-pin", pinned); - return this.$(".post-label-pinned").toggleClass("is-hidden", !pinned); + this.updateButtonState('.action-pin', pinned); + return this.$('.post-label-pinned').toggleClass('is-hidden', !pinned); }, abuse_flaggers: function() { var flagged; flagged = this.model.isFlagged(); - this.updateButtonState(".action-report", flagged); - return this.$(".post-label-reported").toggleClass("is-hidden", !flagged); + this.updateButtonState('.action-report', flagged); + return this.$('.post-label-reported').toggleClass('is-hidden', !flagged); }, closed: function(closed) { - this.updateButtonState(".action-close", closed); - this.$(".post-label-closed").toggleClass("is-hidden", !closed); - return this.$(".display-vote").toggle(closed); + this.updateButtonState('.action-close', closed); + this.$('.post-label-closed').toggleClass('is-hidden', !closed); + return this.$('.display-vote').toggle(closed); } } ); @@ -310,36 +308,36 @@ event.preventDefault(); event.stopPropagation(); this.secondaryActionsExpanded = !this.secondaryActionsExpanded; - this.$(".action-more").toggleClass("is-expanded", this.secondaryActionsExpanded); - this.$(".actions-dropdown") - .toggleClass("is-expanded", this.secondaryActionsExpanded) - .attr("aria-expanded", this.secondaryActionsExpanded); + this.$('.action-more').toggleClass('is-expanded', this.secondaryActionsExpanded); + this.$('.actions-dropdown') + .toggleClass('is-expanded', this.secondaryActionsExpanded) + .attr('aria-expanded', this.secondaryActionsExpanded); if (this.secondaryActionsExpanded) { - if (event.type === "keydown") { - this.$(".action-list-item:first").focus(); + if (event.type === 'keydown') { + this.$('.action-list-item:first').focus(); } - $("body").on("click", this.toggleSecondaryActions); - $("body").on("keydown", this.handleSecondaryActionEscape); - return this.$(".action-list-item").on("blur", this.handleSecondaryActionBlur); + $('body').on('click', this.toggleSecondaryActions); + $('body').on('keydown', this.handleSecondaryActionEscape); + return this.$('.action-list-item').on('blur', this.handleSecondaryActionBlur); } else { - $("body").off("click", this.toggleSecondaryActions); - $("body").off("keydown", this.handleSecondaryActionEscape); - return this.$(".action-list-item").off("blur", this.handleSecondaryActionBlur); + $('body').off('click', this.toggleSecondaryActions); + $('body').off('keydown', this.handleSecondaryActionEscape); + return this.$('.action-list-item').off('blur', this.handleSecondaryActionBlur); } }; DiscussionContentShowView.prototype.handleSecondaryActionEscape = function(event) { if (event.keyCode === 27) { this.toggleSecondaryActions(event); - return this.$(".action-more").focus(); + return this.$('.action-more').focus(); } }; DiscussionContentShowView.prototype.handleSecondaryActionBlur = function(event) { var self = this; return setTimeout(function() { - if (self.secondaryActionsExpanded && self.$(".actions-dropdown :focus").length === 0) { + if (self.secondaryActionsExpanded && self.$('.actions-dropdown :focus').length === 0) { return self.toggleSecondaryActions(event); } }, 10); @@ -348,18 +346,18 @@ DiscussionContentShowView.prototype.toggleFollow = function(event) { var is_subscribing, msg, url; event.preventDefault(); - is_subscribing = !this.model.get("subscribed"); - url = this.model.urlFor(is_subscribing ? "follow" : "unfollow"); + is_subscribing = !this.model.get('subscribed'); + url = this.model.urlFor(is_subscribing ? 'follow' : 'unfollow'); if (is_subscribing) { - msg = gettext("We had some trouble subscribing you to this thread. Please try again."); + msg = gettext('We had some trouble subscribing you to this thread. Please try again.'); } else { - msg = gettext("We had some trouble unsubscribing you from this thread. Please try again."); + msg = gettext('We had some trouble unsubscribing you from this thread. Please try again.'); } return DiscussionUtil.updateWithUndo(this.model, { - "subscribed": is_subscribing + 'subscribed': is_subscribing }, { url: url, - type: "POST", + type: 'POST', $elem: $(event.currentTarget) }, msg); }; @@ -368,27 +366,27 @@ var beforeFunc, is_endorsing, msg, updates, url, self = this; event.preventDefault(); - is_endorsing = !this.model.get("endorsed"); - url = this.model.urlFor("endorse"); + is_endorsing = !this.model.get('endorsed'); + url = this.model.urlFor('endorse'); updates = { endorsed: is_endorsing, endorsement: is_endorsing ? { - username: DiscussionUtil.getUser().get("username"), + username: DiscussionUtil.getUser().get('username'), user_id: DiscussionUtil.getUser().id, time: new Date().toISOString() } : null }; if (this.model.get('thread').get('thread_type') === 'question') { if (is_endorsing) { - msg = gettext("We had some trouble marking this response as an answer. Please try again."); + msg = gettext('We had some trouble marking this response as an answer. Please try again.'); } else { - msg = gettext("We had some trouble removing this response as an answer. Please try again."); + msg = gettext('We had some trouble removing this response as an answer. Please try again.'); } } else { if (is_endorsing) { - msg = gettext("We had some trouble marking this response endorsed. Please try again."); + msg = gettext('We had some trouble marking this response endorsed. Please try again.'); } else { - msg = gettext("We had some trouble removing this endorsement. Please try again."); + msg = gettext('We had some trouble removing this endorsement. Please try again.'); } } return DiscussionUtil.updateWithUndo( @@ -396,13 +394,13 @@ updates, { url: url, - type: "POST", - data: { endorsed: is_endorsing }, + type: 'POST', + data: {endorsed: is_endorsing}, $elem: $(event.currentTarget) }, msg, - function() { return self.trigger("comment:endorse"); } - ).always(this.trigger("comment:endorse")); + function() { return self.trigger('comment:endorse'); } + ).always(this.trigger('comment:endorse')); }; DiscussionContentShowView.prototype.toggleVote = function(event) { @@ -411,16 +409,16 @@ event.preventDefault(); user = DiscussionUtil.getUser(); is_voting = !user.voted(this.model); - url = this.model.urlFor(is_voting ? "upvote" : "unvote"); + url = this.model.urlFor(is_voting ? 'upvote' : 'unvote'); updates = { upvoted_ids: (is_voting ? _.union : _.difference)(user.get('upvoted_ids'), [this.model.id]) }; - if (!$(event.target.closest(".actions-item")).hasClass('is-disabled')) { + if (!$(event.target.closest('.actions-item')).hasClass('is-disabled')) { return DiscussionUtil.updateWithUndo(user, updates, { url: url, - type: "POST", + type: 'POST', $elem: $(event.currentTarget) - }, gettext("We had some trouble saving your vote. Please try again.")).done(function() { + }, gettext('We had some trouble saving your vote. Please try again.')).done(function() { if (is_voting) { return self.model.vote(); } else { @@ -433,18 +431,18 @@ DiscussionContentShowView.prototype.togglePin = function(event) { var is_pinning, msg, url; event.preventDefault(); - is_pinning = !this.model.get("pinned"); - url = this.model.urlFor(is_pinning ? "pinThread" : "unPinThread"); + is_pinning = !this.model.get('pinned'); + url = this.model.urlFor(is_pinning ? 'pinThread' : 'unPinThread'); if (is_pinning) { - msg = gettext("We had some trouble pinning this thread. Please try again."); + msg = gettext('We had some trouble pinning this thread. Please try again.'); } else { - msg = gettext("We had some trouble unpinning this thread. Please try again."); + msg = gettext('We had some trouble unpinning this thread. Please try again.'); } return DiscussionUtil.updateWithUndo(this.model, { pinned: is_pinning }, { url: url, - type: "POST", + type: 'POST', $elem: $(event.currentTarget) }, msg); }; @@ -454,20 +452,20 @@ event.preventDefault(); if (this.model.isFlagged()) { is_flagging = false; - msg = gettext("We had some trouble removing your flag on this post. Please try again."); + msg = gettext('We had some trouble removing your flag on this post. Please try again.'); } else { is_flagging = true; - msg = gettext("We had some trouble reporting this post. Please try again."); + msg = gettext('We had some trouble reporting this post. Please try again.'); } - url = this.model.urlFor(is_flagging ? "flagAbuse" : "unFlagAbuse"); + url = this.model.urlFor(is_flagging ? 'flagAbuse' : 'unFlagAbuse'); updates = { abuse_flaggers: (is_flagging ? _.union : _.difference)( - this.model.get("abuse_flaggers"), [DiscussionUtil.getUser().id] + this.model.get('abuse_flaggers'), [DiscussionUtil.getUser().id] ) }; return DiscussionUtil.updateWithUndo(this.model, updates, { url: url, - type: "POST", + type: 'POST', $elem: $(event.currentTarget) }, msg); }; @@ -477,23 +475,23 @@ event.preventDefault(); is_closing = !this.model.get('closed'); if (is_closing) { - msg = gettext("We had some trouble closing this thread. Please try again."); + msg = gettext('We had some trouble closing this thread. Please try again.'); } else { - msg = gettext("We had some trouble reopening this thread. Please try again."); + msg = gettext('We had some trouble reopening this thread. Please try again.'); } updates = { closed: is_closing }; return DiscussionUtil.updateWithUndo(this.model, updates, { - url: this.model.urlFor("close"), - type: "POST", + url: this.model.urlFor('close'), + type: 'POST', data: updates, $elem: $(event.currentTarget) }, msg); }; DiscussionContentShowView.prototype.getAuthorDisplay = function() { - return _.template($("#post-user-display-template").html())({ + return _.template($('#post-user-display-template').html())({ username: this.model.get('username') || null, user_url: this.model.get('user_url'), is_community_ta: this.model.get('community_ta_authored'), @@ -505,7 +503,7 @@ var endorsement; endorsement = this.model.get('endorsement'); if (endorsement && endorsement.username) { - return _.template($("#post-user-display-template").html())({ + return _.template($('#post-user-display-template').html())({ username: endorsement.username, user_url: DiscussionUtil.urlFor('user_profile', endorsement.user_id), is_community_ta: DiscussionUtil.isTA(endorsement.user_id), @@ -517,8 +515,6 @@ }; return DiscussionContentShowView; - }).call(this, this.DiscussionContentView); } - }).call(window); diff --git a/common/static/common/js/discussion/views/discussion_thread_edit_view.js b/common/static/common/js/discussion/views/discussion_thread_edit_view.js index c693768091..2bf10032a7 100644 --- a/common/static/common/js/discussion/views/discussion_thread_edit_view.js +++ b/common/static/common/js/discussion/views/discussion_thread_edit_view.js @@ -25,15 +25,15 @@ }, render: function() { - var formId = _.uniqueId("form-"), - threadTypeTemplate = edx.HtmlUtils.template($("#thread-type-template").html()), + var formId = _.uniqueId('form-'), + threadTypeTemplate = edx.HtmlUtils.template($('#thread-type-template').html()), $threadTypeSelector = $(threadTypeTemplate({form_id: formId}).toString()), mainTemplate = edx.HtmlUtils.template($('#thread-edit-template').html()); edx.HtmlUtils.setHtml(this.$el, mainTemplate(this.model.toJSON())); this.container.append(this.$el); this.$submitBtn = this.$('.post-update'); this.addField($threadTypeSelector); - this.$("#" + formId + "-post-type-" + this.threadType).attr('checked', true); + this.$('#' + formId + '-post-type-' + this.threadType).attr('checked', true); // Only allow the topic field for course threads, as standalone threads // cannot be moved. if (this.context === 'course') { @@ -58,7 +58,7 @@ save: function() { var title = this.$('.edit-post-title').val(), - threadType = this.$(".post-type-input:checked").val(), + threadType = this.$('.post-type-input:checked').val(), body = this.$('.edit-post-body textarea').val(), postData = { title: title, @@ -87,7 +87,7 @@ this.model.set(postData).unset('abbreviatedBody'); this.trigger('thread:updated'); if (this.threadType !== threadType) { - this.model.set("thread_type", threadType); + this.model.set('thread_type', threadType); this.model.trigger('thread:thread_type_updated'); this.trigger('comment:endorse'); } @@ -105,7 +105,7 @@ cancelHandler: function(event) { event.preventDefault(); - this.trigger("thread:cancel_edit", event); + this.trigger('thread:cancel_edit', event); this.remove(); return this; } diff --git a/common/static/common/js/discussion/views/discussion_thread_list_view.js b/common/static/common/js/discussion/views/discussion_thread_list_view.js index 58061d1520..21be544aaa 100644 --- a/common/static/common/js/discussion/views/discussion_thread_list_view.js +++ b/common/static/common/js/discussion/views/discussion_thread_list_view.js @@ -18,7 +18,7 @@ return child; }; - if (typeof Backbone !== "undefined" && Backbone !== null) { + if (typeof Backbone !== 'undefined' && Backbone !== null) { this.DiscussionThreadListView = (function(_super) { __extends(DiscussionThreadListView, _super); @@ -91,20 +91,20 @@ } DiscussionThreadListView.prototype.events = { - "click .forum-nav-browse": "toggleBrowseMenu", - "keypress .forum-nav-browse-filter-input": function(event) { + 'click .forum-nav-browse': 'toggleBrowseMenu', + 'keypress .forum-nav-browse-filter-input': function(event) { return DiscussionUtil.ignoreEnterKey(event); }, - "keyup .forum-nav-browse-filter-input": "filterTopics", - "click .forum-nav-browse-menu-wrapper": "ignoreClick", - "click .forum-nav-browse-title": "selectTopicHandler", - "keydown .forum-nav-search-input": "performSearch", - "click .fa-search": "performSearch", - "change .forum-nav-sort-control": "sortThreads", - "click .forum-nav-thread-link": "threadSelected", - "click .forum-nav-load-more-link": "loadMorePages", - "change .forum-nav-filter-main-control": "chooseFilter", - "change .forum-nav-filter-cohort-control": "chooseCohort" + 'keyup .forum-nav-browse-filter-input': 'filterTopics', + 'click .forum-nav-browse-menu-wrapper': 'ignoreClick', + 'click .forum-nav-browse-title': 'selectTopicHandler', + 'keydown .forum-nav-search-input': 'performSearch', + 'click .fa-search': 'performSearch', + 'change .forum-nav-sort-control': 'sortThreads', + 'click .forum-nav-thread-link': 'threadSelected', + 'click .forum-nav-load-more-link': 'loadMorePages', + 'change .forum-nav-filter-main-control': 'chooseFilter', + 'change .forum-nav-filter-cohort-control': 'chooseCohort' }; DiscussionThreadListView.prototype.initialize = function(options) { @@ -113,43 +113,43 @@ this.displayedCollection = new Discussion(this.collection.models, { pages: this.collection.pages }); - this.collection.on("change", this.reloadDisplayedCollection); - this.discussionIds = ""; - this.collection.on("reset", function(discussion) { + this.collection.on('change', this.reloadDisplayedCollection); + this.discussionIds = ''; + this.collection.on('reset', function(discussion) { var board; - board = $(".current-board").html(); + board = $('.current-board').html(); self.displayedCollection.current_page = discussion.current_page; self.displayedCollection.pages = discussion.pages; return self.displayedCollection.reset(discussion.models); }); - this.collection.on("add", this.addAndSelectThread); - this.collection.on("thread:remove", this.threadRemoved); + this.collection.on('add', this.addAndSelectThread); + this.collection.on('thread:remove', this.threadRemoved); this.sidebar_padding = 10; this.boardName = null; - this.template = _.template($("#thread-list-template").html()); - this.current_search = ""; + this.template = _.template($('#thread-list-template').html()); + this.current_search = ''; this.mode = 'all'; this.searchAlertCollection = new Backbone.Collection([], { model: Backbone.Model }); - this.searchAlertCollection.on("add", function(searchAlert) { + this.searchAlertCollection.on('add', function(searchAlert) { var content; - content = edx.HtmlUtils.template($("#search-alert-template").html())({ + content = edx.HtmlUtils.template($('#search-alert-template').html())({ 'messageHtml': searchAlert.attributes.message, 'cid': searchAlert.cid, 'css_class': searchAlert.attributes.css_class }); - edx.HtmlUtils.append(self.$(".search-alerts"), content); - return self.$("#search-alert-" + searchAlert.cid + " a.dismiss") - .bind("click", searchAlert, function(event) { + edx.HtmlUtils.append(self.$('.search-alerts'), content); + return self.$('#search-alert-' + searchAlert.cid + ' a.dismiss') + .bind('click', searchAlert, function(event) { return self.removeSearchAlert(event.data.cid); }); }); - this.searchAlertCollection.on("remove", function(searchAlert) { - return self.$("#search-alert-" + searchAlert.cid).remove(); + this.searchAlertCollection.on('remove', function(searchAlert) { + return self.$('#search-alert-' + searchAlert.cid).remove(); }); - return this.searchAlertCollection.on("reset", function() { - return self.$(".search-alerts").empty(); + return this.searchAlertCollection.on('reset', function() { + return self.$('.search-alerts').empty(); }); }; @@ -163,9 +163,9 @@ DiscussionThreadListView.prototype.addSearchAlert = function(message, css_class) { var m; if (typeof css_class === 'undefined' || css_class === null) { - css_class = ""; + css_class = ''; } - m = new Backbone.Model({"message": message, "css_class": css_class}); + m = new Backbone.Model({'message': message, 'css_class': css_class}); this.searchAlertCollection.add(m); return m; }; @@ -183,8 +183,8 @@ this.clearSearchAlerts(); thread_id = thread.get('id'); $content = this.renderThread(thread); - current_el = this.$(".forum-nav-thread[data-id=" + thread_id + "]"); - active = current_el.has(".forum-nav-thread-link.is-active").length !== 0; + current_el = this.$('.forum-nav-thread[data-id=' + thread_id + ']'); + active = current_el.has('.forum-nav-thread-link.is-active').length !== 0; current_el.replaceWith($content); this.showMetadataAccordingToSort(); if (active) { @@ -200,13 +200,13 @@ DiscussionThreadListView.prototype.addAndSelectThread = function(thread) { var commentable_id, menuItem, self = this; - commentable_id = thread.get("commentable_id"); - menuItem = this.$(".forum-nav-browse-menu-item[data-discussion-id]").filter(function() { - return $(this).data("discussion-id") === commentable_id; + commentable_id = thread.get('commentable_id'); + menuItem = this.$('.forum-nav-browse-menu-item[data-discussion-id]').filter(function() { + return $(this).data('discussion-id') === commentable_id; }); this.setCurrentTopicDisplay(this.getPathText(menuItem)); return this.retrieveDiscussion(commentable_id, function() { - return self.trigger("thread:created", thread.get('id')); + return self.trigger('thread:created', thread.get('id')); }); }; @@ -216,10 +216,10 @@ windowHeight; scrollTop = $(window).scrollTop(); windowHeight = $(window).height(); - discussionBody = $(".discussion-column"); + discussionBody = $('.discussion-column'); discussionsBodyTop = discussionBody[0] ? discussionBody.offset().top : void 0; discussionsBodyBottom = discussionsBodyTop + discussionBody.outerHeight(); - sidebar = $(".forum-nav"); + sidebar = $('.forum-nav'); if (scrollTop > discussionsBodyTop - this.sidebar_padding) { sidebar.css('top', scrollTop - discussionsBodyTop + this.sidebar_padding); } else { @@ -232,9 +232,9 @@ sidebarHeight = sidebarHeight - this.sidebar_padding - amount; sidebarHeight = Math.min(sidebarHeight + 1, discussionBody.outerHeight()); sidebar.css('height', sidebarHeight); - headerHeight = this.$(".forum-nav-header").outerHeight(); - refineBarHeight = this.$(".forum-nav-refine-bar").outerHeight(); - browseFilterHeight = this.$(".forum-nav-browse-filter").outerHeight(); + headerHeight = this.$('.forum-nav-header').outerHeight(); + refineBarHeight = this.$('.forum-nav-refine-bar').outerHeight(); + browseFilterHeight = this.$('.forum-nav-browse-filter').outerHeight(); this.$('.forum-nav-thread-list') .css('height', (sidebarHeight - headerHeight - refineBarHeight - 2) + 'px'); this.$('.forum-nav-browse-menu') @@ -248,21 +248,21 @@ DiscussionThreadListView.prototype.render = function() { var self = this, $elem = this.template({ - isCohorted: this.courseSettings.get("is_cohorted"), + isCohorted: this.courseSettings.get('is_cohorted'), isPrivilegedUser: DiscussionUtil.isPrivilegedUser() }); this.timer = 0; this.$el.empty(); this.$el.append($elem); - this.$(".forum-nav-sort-control option").removeProp("selected"); - this.$(".forum-nav-sort-control option[value=" + this.collection.sort_preference + "]") - .prop("selected", true); - $(window).bind("load scroll resize", this.updateSidebar); - this.displayedCollection.on("reset", this.renderThreads); - this.displayedCollection.on("thread:remove", this.renderThreads); - this.displayedCollection.on("change:commentable_id", function() { - if (self.mode === "commentables") { - return self.retrieveDiscussions(self.discussionIds.split(",")); + this.$('.forum-nav-sort-control option').removeProp('selected'); + this.$('.forum-nav-sort-control option[value=' + this.collection.sort_preference + ']') + .prop('selected', true); + $(window).bind('load scroll resize', this.updateSidebar); + this.displayedCollection.on('reset', this.renderThreads); + this.displayedCollection.on('thread:remove', this.renderThreads); + this.displayedCollection.on('change:commentable_id', function() { + if (self.mode === 'commentables') { + return self.retrieveDiscussions(self.discussionIds.split(',')); } }); this.renderThreads(); @@ -271,44 +271,44 @@ DiscussionThreadListView.prototype.renderThreads = function() { var $content, thread, i, len; - this.$(".forum-nav-thread-list").empty(); + this.$('.forum-nav-thread-list').empty(); for (i = 0, len = this.displayedCollection.models.length; i < len; i++) { thread = this.displayedCollection.models[i]; $content = this.renderThread(thread); - this.$(".forum-nav-thread-list").append($content); + this.$('.forum-nav-thread-list').append($content); } this.showMetadataAccordingToSort(); this.renderMorePages(); this.updateSidebar(); - this.trigger("threads:rendered"); + this.trigger('threads:rendered'); }; DiscussionThreadListView.prototype.showMetadataAccordingToSort = function() { var commentCounts, voteCounts; - voteCounts = this.$(".forum-nav-thread-votes-count"); - commentCounts = this.$(".forum-nav-thread-comments-count"); + voteCounts = this.$('.forum-nav-thread-votes-count'); + commentCounts = this.$('.forum-nav-thread-comments-count'); voteCounts.hide(); commentCounts.hide(); - switch (this.$(".forum-nav-sort-control").val()) { - case "activity": - case "comments": - return commentCounts.show(); - case "votes": - return voteCounts.show(); + switch (this.$('.forum-nav-sort-control').val()) { + case 'activity': + case 'comments': + return commentCounts.show(); + case 'votes': + return voteCounts.show(); } }; DiscussionThreadListView.prototype.renderMorePages = function() { if (this.displayedCollection.hasMorePages()) { edx.HtmlUtils.append( - this.$(".forum-nav-thread-list"), - edx.HtmlUtils.template($("#nav-load-more-link").html())({}) + this.$('.forum-nav-thread-list'), + edx.HtmlUtils.template($('#nav-load-more-link').html())({}) ); } }; DiscussionThreadListView.prototype.getLoadingContent = function(srText) { - return edx.HtmlUtils.template($("#nav-loading-template").html())({srText: srText}); + return edx.HtmlUtils.template($('#nav-loading-template').html())({srText: srText}); }; DiscussionThreadListView.prototype.loadMorePages = function(event) { @@ -317,69 +317,69 @@ if (event) { event.preventDefault(); } - loadMoreElem = this.$(".forum-nav-load-more"); + loadMoreElem = this.$('.forum-nav-load-more'); loadMoreElem.empty(); - edx.HtmlUtils.append(loadMoreElem, this.getLoadingContent(gettext("Loading more threads"))); - loadingElem = loadMoreElem.find(".forum-nav-loading"); + edx.HtmlUtils.append(loadMoreElem, this.getLoadingContent(gettext('Loading more threads'))); + loadingElem = loadMoreElem.find('.forum-nav-loading'); DiscussionUtil.makeFocusTrap(loadingElem); loadingElem.focus(); options = { filter: this.filter }; switch (this.mode) { - case 'search': - options.search_text = this.current_search; - if (this.group_id) { - options.group_id = this.group_id; - } - break; - case 'followed': - options.user_id = window.user.id; - break; - case 'commentables': - options.commentable_ids = this.discussionIds; - if (this.group_id) { - options.group_id = this.group_id; - } - break; - case 'all': - if (this.group_id) { - options.group_id = this.group_id; - } + case 'search': + options.search_text = this.current_search; + if (this.group_id) { + options.group_id = this.group_id; + } + break; + case 'followed': + options.user_id = window.user.id; + break; + case 'commentables': + options.commentable_ids = this.discussionIds; + if (this.group_id) { + options.group_id = this.group_id; + } + break; + case 'all': + if (this.group_id) { + options.group_id = this.group_id; + } } _ref = this.collection.last(); lastThread = _ref ? _ref.get('id') : void 0; if (lastThread) { - this.once("threads:rendered", function() { + this.once('threads:rendered', function() { var classSelector = ".forum-nav-thread[data-id='" + lastThread + "'] + .forum-nav-thread " + - ".forum-nav-thread-link"; + '.forum-nav-thread-link'; return $(classSelector).focus(); }); } else { - this.once("threads:rendered", function() { - var _ref1 = $(".forum-nav-thread-link").first(); + this.once('threads:rendered', function() { + var _ref1 = $('.forum-nav-thread-link').first(); return _ref1 ? _ref1.focus() : void 0; }); } error = function() { self.renderThreads(); DiscussionUtil.discussionAlert( - gettext("Sorry"), gettext("We had some trouble loading more threads. Please try again.") + gettext('Sorry'), gettext('We had some trouble loading more threads. Please try again.') ); }; return this.collection.retrieveAnotherPage(this.mode, options, { - sort_key: this.$(".forum-nav-sort-control").val() + sort_key: this.$('.forum-nav-sort-control').val() }, error); }; DiscussionThreadListView.prototype.renderThread = function(thread) { var content, unreadCount; - content = $(_.template($("#thread-list-item-template").html())(thread.toJSON())); - unreadCount = thread.get('unread_comments_count') + (thread.get("read") ? 0 : 1); + content = $(_.template($('#thread-list-item-template').html())(thread.toJSON())); + unreadCount = thread.get('unread_comments_count') + (thread.get('read') ? 0 : 1); if (unreadCount > 0) { content.find('.forum-nav-thread-comments-count').attr( - "data-tooltip", + 'data-tooltip', edx.StringUtils.interpolate( ngettext('{unread_count} new comment', '{unread_count} new comments', unreadCount), {unread_count: unreadCount}, @@ -392,42 +392,42 @@ DiscussionThreadListView.prototype.threadSelected = function(e) { var thread_id; - thread_id = $(e.target).closest(".forum-nav-thread").attr("data-id"); + thread_id = $(e.target).closest('.forum-nav-thread').attr('data-id'); this.setActiveThread(thread_id); - this.trigger("thread:selected", thread_id); + this.trigger('thread:selected', thread_id); return false; }; DiscussionThreadListView.prototype.threadRemoved = function(thread) { - this.trigger("thread:removed", thread); + this.trigger('thread:removed', thread); }; DiscussionThreadListView.prototype.setActiveThread = function(thread_id) { var $srElem; - this.$(".forum-nav-thread-link").find(".sr").remove(); + this.$('.forum-nav-thread-link').find('.sr').remove(); this.$(".forum-nav-thread[data-id!='" + thread_id + "'] .forum-nav-thread-link") - .removeClass("is-active"); + .removeClass('is-active'); $srElem = edx.HtmlUtils.joinHtml( edx.HtmlUtils.HTML(''), - edx.HtmlUtils.ensureHtml(gettext("Current conversation")), + edx.HtmlUtils.ensureHtml(gettext('Current conversation')), edx.HtmlUtils.HTML('') ).toString(); this.$(".forum-nav-thread[data-id='" + thread_id + "'] .forum-nav-thread-link") - .addClass("is-active").find(".forum-nav-thread-wrapper-1") + .addClass('is-active').find('.forum-nav-thread-wrapper-1') .prepend($srElem); }; DiscussionThreadListView.prototype.goHome = function() { var url, $tpl_content; - this.template = _.template($("#discussion-home-template").html()); + this.template = _.template($('#discussion-home-template').html()); $tpl_content = $(this.template()); - $(".forum-content").empty().append($tpl_content); - $(".forum-nav-thread-list a").removeClass("is-active").find(".sr").remove(); - $("input.email-setting").bind("click", this.updateEmailNotifications); - url = DiscussionUtil.urlFor("notifications_status", window.user.get("id")); + $('.forum-content').empty().append($tpl_content); + $('.forum-nav-thread-list a').removeClass('is-active').find('.sr').remove(); + $('input.email-setting').bind('click', this.updateEmailNotifications); + url = DiscussionUtil.urlFor('notifications_status', window.user.get('id')); DiscussionUtil.safeAjax({ url: url, - type: "GET", + type: 'GET', success: function(response) { $('input.email-setting').prop('checked', response.status); } @@ -435,26 +435,26 @@ }; DiscussionThreadListView.prototype.isBrowseMenuVisible = function() { - return this.$(".forum-nav-browse-menu-wrapper").is(":visible"); + return this.$('.forum-nav-browse-menu-wrapper').is(':visible'); }; DiscussionThreadListView.prototype.showBrowseMenu = function() { if (!this.isBrowseMenuVisible()) { - this.$(".forum-nav-browse").addClass("is-active"); - this.$(".forum-nav-browse-menu-wrapper").show(); - this.$(".forum-nav-thread-list-wrapper").hide(); - $(".forum-nav-browse-filter-input").focus(); - $("body").bind("click", this.hideBrowseMenu); + this.$('.forum-nav-browse').addClass('is-active'); + this.$('.forum-nav-browse-menu-wrapper').show(); + this.$('.forum-nav-thread-list-wrapper').hide(); + $('.forum-nav-browse-filter-input').focus(); + $('body').bind('click', this.hideBrowseMenu); return this.updateSidebar(); } }; DiscussionThreadListView.prototype.hideBrowseMenu = function() { if (this.isBrowseMenuVisible()) { - this.$(".forum-nav-browse").removeClass("is-active"); - this.$(".forum-nav-browse-menu-wrapper").hide(); - this.$(".forum-nav-thread-list-wrapper").show(); - $("body").unbind("click", this.hideBrowseMenu); + this.$('.forum-nav-browse').removeClass('is-active'); + this.$('.forum-nav-browse-menu-wrapper').hide(); + this.$('.forum-nav-thread-list-wrapper').show(); + $('body').unbind('click', this.hideBrowseMenu); return this.updateSidebar(); } }; @@ -471,18 +471,18 @@ DiscussionThreadListView.prototype.getPathText = function(item) { var path, pathTitles; - path = item.parents(".forum-nav-browse-menu-item").andSelf(); - pathTitles = path.children(".forum-nav-browse-title").map(function(i, elem) { + path = item.parents('.forum-nav-browse-menu-item').andSelf(); + pathTitles = path.children('.forum-nav-browse-title').map(function(i, elem) { return $(elem).text(); }).get(); - return pathTitles.join(" / "); + return pathTitles.join(' / '); }; DiscussionThreadListView.prototype.filterTopics = function(event) { var items, query, self = this; query = $(event.target).val(); - items = this.$(".forum-nav-browse-menu-item"); + items = this.$('.forum-nav-browse-menu-item'); if (query.length === 0) { return items.show(); } else { @@ -490,13 +490,13 @@ return items.each(function(i, item) { var path, pathText; item = $(item); - if (!item.is(":visible")) { + if (!item.is(':visible')) { pathText = self.getPathText(item).toLowerCase(); - if (query.split(" ").every(function(term) { - return pathText.search(term.toLowerCase()) !== -1; - })) { - path = item.parents(".forum-nav-browse-menu-item").andSelf(); - return path.add(item.find(".forum-nav-browse-menu-item")).show(); + if (query.split(' ').every(function(term) { + return pathText.search(term.toLowerCase()) !== -1; + })) { + path = item.parents('.forum-nav-browse-menu-item').andSelf(); + return path.add(item.find('.forum-nav-browse-menu-item')).show(); } } }); @@ -504,20 +504,20 @@ }; DiscussionThreadListView.prototype.setCurrentTopicDisplay = function(text) { - return this.$(".forum-nav-browse-current").text(this.fitName(text)); + return this.$('.forum-nav-browse-current').text(this.fitName(text)); }; DiscussionThreadListView.prototype.getNameWidth = function(name) { var $test, width; - $test = $("
        "); + $test = $('
        '); $test.css({ - "font-size": this.$(".forum-nav-browse-current").css('font-size'), + 'font-size': this.$('.forum-nav-browse-current').css('font-size'), opacity: 0, position: 'absolute', left: -1000, top: -1000 }); - $("body").append($test); + $('body').append($test); $test.text(name); width = $test.width(); $test.remove(); @@ -526,28 +526,28 @@ DiscussionThreadListView.prototype.fitName = function(name) { var partialName, path, prefix, rawName, width, x; - this.maxNameWidth = this.$(".forum-nav-browse").width() - - this.$(".forum-nav-browse .icon").outerWidth(true) - - this.$(".forum-nav-browse-drop-arrow").outerWidth(true); + this.maxNameWidth = this.$('.forum-nav-browse').width() - + this.$('.forum-nav-browse .icon').outerWidth(true) - + this.$('.forum-nav-browse-drop-arrow').outerWidth(true); width = this.getNameWidth(name); if (width < this.maxNameWidth) { return name; } path = (function() { var _i, _len, _ref, _results; - _ref = name.split("/"); + _ref = name.split('/'); _results = []; for (_i = 0, _len = _ref.length; _i < _len; _i++) { x = _ref[_i]; - _results.push(x.replace(/^\s+|\s+$/g, "")); + _results.push(x.replace(/^\s+|\s+$/g, '')); } return _results; })(); - prefix = ""; + prefix = ''; while (path.length > 1) { - prefix = gettext("…") + "/"; + prefix = gettext('…') + '/'; path.shift(); - partialName = prefix + path.join("/"); + partialName = prefix + path.join('/'); if (this.getNameWidth(partialName) < this.maxNameWidth) { return partialName; } @@ -556,7 +556,7 @@ name = prefix + rawName; while (this.getNameWidth(name) > this.maxNameWidth) { rawName = rawName.slice(0, rawName.length - 1); - name = prefix + rawName + gettext("…"); + name = prefix + rawName + gettext('…'); } return name; }; @@ -572,25 +572,25 @@ this.clearSearch(); item = $target.closest('.forum-nav-browse-menu-item'); this.setCurrentTopicDisplay(this.getPathText(item)); - if (item.hasClass("forum-nav-browse-menu-all")) { - this.discussionIds = ""; + if (item.hasClass('forum-nav-browse-menu-all')) { + this.discussionIds = ''; this.$('.forum-nav-filter-cohort').show(); return this.retrieveAllThreads(); - } else if (item.hasClass("forum-nav-browse-menu-following")) { + } else if (item.hasClass('forum-nav-browse-menu-following')) { this.retrieveFollowed(); return this.$('.forum-nav-filter-cohort').hide(); } else { - allItems = item.find(".forum-nav-browse-menu-item").andSelf(); - discussionIds = allItems.filter("[data-discussion-id]").map(function(i, elem) { - return $(elem).data("discussion-id"); + allItems = item.find('.forum-nav-browse-menu-item').andSelf(); + discussionIds = allItems.filter('[data-discussion-id]').map(function(i, elem) { + return $(elem).data('discussion-id'); }).get(); this.retrieveDiscussions(discussionIds); - return this.$(".forum-nav-filter-cohort").toggle(item.data('cohorted') === true); + return this.$('.forum-nav-filter-cohort').toggle(item.data('cohorted') === true); } }; DiscussionThreadListView.prototype.chooseFilter = function() { - this.filter = $(".forum-nav-filter-main-control :selected").val(); + this.filter = $('.forum-nav-filter-main-control :selected').val(); return this.retrieveFirstPage(); }; @@ -601,10 +601,10 @@ DiscussionThreadListView.prototype.retrieveDiscussion = function(discussion_id, callback) { var url, self = this; - url = DiscussionUtil.urlFor("retrieve_discussion", discussion_id); + url = DiscussionUtil.urlFor('retrieve_discussion', discussion_id); return DiscussionUtil.safeAjax({ url: url, - type: "GET", + type: 'GET', success: function(response) { self.collection.current_page = response.page; self.collection.pages = response.num_pages; @@ -636,7 +636,7 @@ }; DiscussionThreadListView.prototype.sortThreads = function(event) { - this.displayedCollection.setSortComparator(this.$(".forum-nav-sort-control").val()); + this.displayedCollection.setSortComparator(this.$('.forum-nav-sort-control').val()); return this.retrieveFirstPage(event); }; @@ -649,8 +649,8 @@ if (event.which === 13 || event.type === 'click') { event.preventDefault(); this.hideBrowseMenu(); - this.setCurrentTopicDisplay(gettext("Search Results")); - text = this.$(".forum-nav-search-input").val(); + this.setCurrentTopicDisplay(gettext('Search Results')); + text = this.$('.forum-nav-search-input').val(); return this.searchFor(text); } }; @@ -661,7 +661,7 @@ this.clearFilters(); this.mode = 'search'; this.current_search = text; - url = DiscussionUtil.urlFor("search"); + url = DiscussionUtil.urlFor('search'); /* TODO: This might be better done by setting discussion.current_page=0 and calling discussion.loadMorePages @@ -670,28 +670,28 @@ */ return DiscussionUtil.safeAjax({ - $elem: this.$(".forum-nav-search-input"), + $elem: this.$('.forum-nav-search-input'), data: { text: text }, url: url, - type: "GET", + type: 'GET', dataType: 'json', $loading: $, loadingCallback: function() { - var element = self.$(".forum-nav-thread-list"); + var element = self.$('.forum-nav-thread-list'); element.empty(); edx.HtmlUtils.append( element, edx.HtmlUtils.joinHtml( edx.HtmlUtils.HTML("
      3. "), - self.getLoadingContent(gettext("Loading thread list")), - edx.HtmlUtils.HTML("
      4. ") + self.getLoadingContent(gettext('Loading thread list')), + edx.HtmlUtils.HTML('') ) ); }, loadedCallback: function() { - return self.$(".forum-nav-thread-list .forum-nav-load-more").remove(); + return self.$('.forum-nav-thread-list .forum-nav-load-more').remove(); }, success: function(response, textStatus) { var message, noResponseMsg; @@ -710,13 +710,13 @@ message = edx.HtmlUtils.interpolateHtml( noResponseMsg, { - "original_query": edx.HtmlUtils.joinHtml( - edx.HtmlUtils.HTML(""), text, edx.HtmlUtils.HTML("") + 'original_query': edx.HtmlUtils.joinHtml( + edx.HtmlUtils.HTML(''), text, edx.HtmlUtils.HTML('') ), - "suggested_query": edx.HtmlUtils.joinHtml( - edx.HtmlUtils.HTML(""), - response.corrected_text , - edx.HtmlUtils.HTML("") + 'suggested_query': edx.HtmlUtils.joinHtml( + edx.HtmlUtils.HTML(''), + response.corrected_text, + edx.HtmlUtils.HTML('') ) } ); @@ -739,8 +739,8 @@ data: { username: text }, - url: DiscussionUtil.urlFor("users"), - type: "GET", + url: DiscussionUtil.urlFor('users'), + type: 'GET', dataType: 'json', error: function() {}, success: function(response) { @@ -749,14 +749,14 @@ username = edx.HtmlUtils.joinHtml( edx.HtmlUtils.interpolateHtml( edx.HtmlUtils.HTML('
        '), - {url: DiscussionUtil.urlFor("user_profile", response.users[0].id)} + {url: DiscussionUtil.urlFor('user_profile', response.users[0].id)} ), response.users[0].username, - edx.HtmlUtils.HTML("") + edx.HtmlUtils.HTML('') ); message = edx.HtmlUtils.interpolateHtml( - gettext('Show posts by {username}.'), {"username": username} + gettext('Show posts by {username}.'), {'username': username} ); return self.addSearchAlert(message, 'search-by-user'); } @@ -765,14 +765,14 @@ }; DiscussionThreadListView.prototype.clearSearch = function() { - this.$(".forum-nav-search-input").val(""); - this.current_search = ""; + this.$('.forum-nav-search-input').val(''); + this.current_search = ''; return this.clearSearchAlerts(); }; DiscussionThreadListView.prototype.clearFilters = function() { - this.$(".forum-nav-filter-main-control").val("all"); - return this.$(".forum-nav-filter-cohort-control").val("all"); + this.$('.forum-nav-filter-main-control').val('all'); + return this.$('.forum-nav-filter-cohort-control').val('all'); }; DiscussionThreadListView.prototype.retrieveFollowed = function() { @@ -784,10 +784,10 @@ var $checkbox, checked, urlName; $checkbox = $('input.email-setting'); checked = $checkbox.prop('checked'); - urlName = (checked) ? "enable_notifications" : "disable_notifications"; + urlName = (checked) ? 'enable_notifications' : 'disable_notifications'; DiscussionUtil.safeAjax({ url: DiscussionUtil.urlFor(urlName), - type: "POST", + type: 'POST', error: function() { $checkbox.prop('checked', !checked); } @@ -795,8 +795,6 @@ }; return DiscussionThreadListView; - }).call(this, Backbone.View); } - }).call(window); diff --git a/common/static/common/js/discussion/views/discussion_thread_profile_view.js b/common/static/common/js/discussion/views/discussion_thread_profile_view.js index 96b84381bb..5d6b65c928 100644 --- a/common/static/common/js/discussion/views/discussion_thread_profile_view.js +++ b/common/static/common/js/discussion/views/discussion_thread_profile_view.js @@ -18,9 +18,8 @@ return child; }; - if (typeof Backbone !== "undefined" && Backbone !== null) { + if (typeof Backbone !== 'undefined' && Backbone !== null) { this.DiscussionThreadProfileView = (function(_super) { - __extends(DiscussionThreadProfileView, _super); function DiscussionThreadProfileView() { @@ -44,16 +43,16 @@ } edx.HtmlUtils.setHtml( this.$el, - edx.HtmlUtils.template($("#profile-thread-template").html())(params) + edx.HtmlUtils.template($('#profile-thread-template').html())(params) ); - this.$("span.timeago").timeago(); - DiscussionUtil.typesetMathJax(this.$(".post-body")); + this.$('span.timeago').timeago(); + DiscussionUtil.typesetMathJax(this.$('.post-body')); return this; }; DiscussionThreadProfileView.prototype.convertMath = function() { var htmlSnippet = DiscussionUtil.markdownWithHighlight(this.model.get('body')); - this.model.set('markdownBody', htmlSnippet); + this.model.set('markdownBody', htmlSnippet); }; DiscussionThreadProfileView.prototype.abbreviateBody = function() { @@ -63,8 +62,6 @@ }; return DiscussionThreadProfileView; - })(Backbone.View); } - }).call(window); diff --git a/common/static/common/js/discussion/views/discussion_thread_show_view.js b/common/static/common/js/discussion/views/discussion_thread_show_view.js index 6e13898b69..9e01c6326b 100644 --- a/common/static/common/js/discussion/views/discussion_thread_show_view.js +++ b/common/static/common/js/discussion/views/discussion_thread_show_view.js @@ -18,9 +18,8 @@ return child; }; - if (typeof Backbone !== "undefined" && Backbone !== null) { + if (typeof Backbone !== 'undefined' && Backbone !== null) { this.DiscussionThreadShowView = (function(_super) { - __extends(DiscussionThreadShowView, _super); function DiscussionThreadShowView() { @@ -30,9 +29,9 @@ DiscussionThreadShowView.prototype.initialize = function(options) { var _ref; DiscussionThreadShowView.__super__.initialize.call(this); - this.mode = options.mode || "inline"; - if ((_ref = this.mode) !== "tab" && _ref !== "inline") { - throw new Error("invalid mode: " + this.mode); + this.mode = options.mode || 'inline'; + if ((_ref = this.mode) !== 'tab' && _ref !== 'inline') { + throw new Error('invalid mode: ' + this.mode); } }; @@ -45,7 +44,7 @@ cid: this.model.cid, readOnly: $('.discussion-module').data('read-only') }, this.model.attributes); - return edx.HtmlUtils.template($("#thread-show-template").html())(context); + return edx.HtmlUtils.template($('#thread-show-template').html())(context); }; DiscussionThreadShowView.prototype.render = function() { @@ -55,28 +54,26 @@ ); this.delegateEvents(); this.renderAttrs(); - this.$("span.timeago").timeago(); + this.$('span.timeago').timeago(); this.convertMath(); - this.$(".post-body"); - this.$("h1,h3"); + this.$('.post-body'); + this.$('h1,h3'); return this; }; DiscussionThreadShowView.prototype.convertMath = function() { - DiscussionUtil.convertMath(this.$(".post-body")); + DiscussionUtil.convertMath(this.$('.post-body')); }; DiscussionThreadShowView.prototype.edit = function(event) { - return this.trigger("thread:edit", event); + return this.trigger('thread:edit', event); }; DiscussionThreadShowView.prototype._delete = function(event) { - return this.trigger("thread:_delete", event); + return this.trigger('thread:_delete', event); }; return DiscussionThreadShowView; - })(DiscussionContentShowView); } - }).call(window); diff --git a/common/static/common/js/discussion/views/discussion_thread_view.js b/common/static/common/js/discussion/views/discussion_thread_view.js index 8a859e77cb..9ae6ff5ce8 100644 --- a/common/static/common/js/discussion/views/discussion_thread_view.js +++ b/common/static/common/js/discussion/views/discussion_thread_view.js @@ -21,7 +21,7 @@ return child; }; - if (typeof Backbone !== "undefined" && Backbone !== null) { + if (typeof Backbone !== 'undefined' && Backbone !== null) { this.DiscussionThreadView = (function(_super) { var INITIAL_RESPONSE_PAGE_SIZE, SUBSEQUENT_RESPONSE_PAGE_SIZE; @@ -61,10 +61,10 @@ SUBSEQUENT_RESPONSE_PAGE_SIZE = 100; DiscussionThreadView.prototype.events = { - "click .discussion-submit-post": "submitComment", - "click .add-response-btn": "scrollToAddResponse", - "click .forum-thread-expand": "expand", - "click .forum-thread-collapse": "collapse" + 'click .discussion-submit-post': 'submitComment', + 'click .add-response-btn': 'scrollToAddResponse', + 'click .forum-thread-expand': 'expand', + 'click .forum-thread-collapse': 'collapse' }; DiscussionThreadView.prototype.$ = function(selector) { @@ -72,23 +72,23 @@ }; DiscussionThreadView.prototype.isQuestion = function() { - return this.model.get("thread_type") === "question"; + return this.model.get('thread_type') === 'question'; }; DiscussionThreadView.prototype.initialize = function(options) { var _ref, self = this; DiscussionThreadView.__super__.initialize.call(this); - this.mode = options.mode || "inline"; - this.context = options.context || "course"; + this.mode = options.mode || 'inline'; + this.context = options.context || 'course'; this.options = _.extend({}, options); - if ((_ref = this.mode) !== "tab" && _ref !== "inline") { - throw new Error("invalid mode: " + this.mode); + if ((_ref = this.mode) !== 'tab' && _ref !== 'inline') { + throw new Error('invalid mode: ' + this.mode); } - this.readOnly = $(".discussion-module").data('read-only'); - this.model.collection.on("reset", function(collection) { + this.readOnly = $('.discussion-module').data('read-only'); + this.model.collection.on('reset', function(collection) { var id; - id = self.model.get("id"); + id = self.model.get('id'); if (collection.get(id)) { self.model = collection.get(id); self.rerender(); @@ -120,14 +120,14 @@ DiscussionThreadView.prototype.renderTemplate = function() { var container, templateData; - this.template = _.template($("#thread-template").html()); - container = $("#discussion-container"); + this.template = _.template($('#thread-template').html()); + container = $('#discussion-container'); if (!container.length) { - container = $(".discussion-module"); + container = $('.discussion-module'); } templateData = _.extend(this.model.toJSON(), { readOnly: this.readOnly, - can_create_comment: container.data("user-create-comment") + can_create_comment: container.data('user-create-comment') }); return this.template(templateData); }; @@ -140,24 +140,24 @@ this.delegateEvents(); this.renderShowView(); this.renderAttrs(); - this.$("span.timeago").timeago(); - this.makeWmdEditor("reply-body"); + this.$('span.timeago').timeago(); + this.makeWmdEditor('reply-body'); this.renderAddResponseButton(); - this.responses.on("add", function(response) { - return self.renderResponseToList(response, ".js-response-list", {}); + this.responses.on('add', function(response) { + return self.renderResponseToList(response, '.js-response-list', {}); }); if (this.isQuestion()) { - this.markedAnswers.on("add", function(response) { - return self.renderResponseToList(response, ".js-marked-answer-list", { + this.markedAnswers.on('add', function(response) { + return self.renderResponseToList(response, '.js-marked-answer-list', { collapseComments: true }); }); } - if (this.mode === "tab") { + if (this.mode === 'tab') { setTimeout(function() { return self.loadInitialResponses(); }, 100); - return this.$(".post-tools").hide(); + return this.$('.post-tools').hide(); } else { return this.collapse(); } @@ -165,10 +165,10 @@ DiscussionThreadView.prototype.attrRenderer = $.extend({}, DiscussionContentView.prototype.attrRenderer, { closed: function(closed) { - this.$(".discussion-reply-new").toggle(!closed); + this.$('.discussion-reply-new').toggle(!closed); this.$('.comment-form').closest('li').toggle(!closed); - this.$(".action-vote").toggle(!closed); - this.$(".display-vote").toggle(closed); + this.$('.action-vote').toggle(!closed); + this.$('.display-vote').toggle(closed); return this.renderAddResponseButton(); } }); @@ -177,12 +177,12 @@ if (event) { event.preventDefault(); } - this.$el.addClass("expanded"); - this.$el.find(".post-body").text(this.model.get("body")); + this.$el.addClass('expanded'); + this.$el.find('.post-body').text(this.model.get('body')); this.showView.convertMath(); - this.$el.find(".forum-thread-expand").hide(); - this.$el.find(".forum-thread-collapse").show(); - this.$el.find(".post-extended-content").show(); + this.$el.find('.forum-thread-expand').hide(); + this.$el.find('.forum-thread-collapse').show(); + this.$el.find('.post-extended-content').show(); if (!this.loadedResponses) { return this.loadInitialResponses(); } @@ -192,28 +192,31 @@ if (event) { event.preventDefault(); } - this.$el.removeClass("expanded"); - this.$el.find(".post-body").text(this.getAbbreviatedBody()); + this.$el.removeClass('expanded'); + this.$el.find('.post-body').text(this.getAbbreviatedBody()); this.showView.convertMath(); - this.$el.find(".forum-thread-expand").show(); - this.$el.find(".forum-thread-collapse").hide(); - return this.$el.find(".post-extended-content").hide(); + this.$el.find('.forum-thread-expand').show(); + this.$el.find('.forum-thread-collapse').hide(); + return this.$el.find('.post-extended-content').hide(); }; DiscussionThreadView.prototype.getAbbreviatedBody = function() { var abbreviated, cached; - cached = this.model.get("abbreviatedBody"); + cached = this.model.get('abbreviatedBody'); if (cached) { return cached; } else { - abbreviated = DiscussionUtil.abbreviateString(this.model.get("body"), 140); - this.model.set("abbreviatedBody", abbreviated); + abbreviated = DiscussionUtil.abbreviateString(this.model.get('body'), 140); + this.model.set('abbreviatedBody', abbreviated); return abbreviated; } }; DiscussionThreadView.prototype.cleanup = function() { - if (this.responsesRequest) { + // jQuery.ajax after 1.5 returns a jqXHR which doesn't implement .abort + // but I don't feel confident enough about what's going on here to remove this code + // so just check to make sure we can abort before we try to + if (this.responsesRequest && this.responsesRequest.abort) { return this.responsesRequest.abort(); } }; @@ -221,7 +224,7 @@ DiscussionThreadView.prototype.loadResponses = function(responseLimit, $elem, firstLoad) { var takeFocus, self = this; - takeFocus = this.mode === "tab" ? false : true; + takeFocus = this.mode === 'tab' ? false : true; this.responsesRequest = DiscussionUtil.safeAjax({ url: DiscussionUtil.urlFor( 'retrieve_single_thread', this.model.get('commentable_id'), this.model.id @@ -249,7 +252,7 @@ data.content.non_endorsed_resp_total : data.content.resp_total ); - self.trigger("thread:responses:rendered"); + self.trigger('thread:responses:rendered'); self.loadedResponses = true; return self.$el.find('.discussion-article[data-id="' + self.model.id + '"]').focus(); }, @@ -259,18 +262,18 @@ } if (xhr.status === 404) { DiscussionUtil.discussionAlert( - gettext("Sorry"), - gettext("The thread you selected has been deleted. Please select another thread.") + gettext('Sorry'), + gettext('The thread you selected has been deleted. Please select another thread.') ); } else if (firstLoad) { DiscussionUtil.discussionAlert( - gettext("Sorry"), - gettext("We had some trouble loading responses. Please reload the page.") + gettext('Sorry'), + gettext('We had some trouble loading responses. Please reload the page.') ); } else { DiscussionUtil.discussionAlert( - gettext("Sorry"), - gettext("We had some trouble loading more responses. Please try again.") + gettext('Sorry'), + gettext('We had some trouble loading more responses. Please try again.') ); } } @@ -278,7 +281,7 @@ }; DiscussionThreadView.prototype.loadInitialResponses = function() { - return this.loadResponses(INITIAL_RESPONSE_PAGE_SIZE, this.$el.find(".js-response-list"), true); + return this.loadResponses(INITIAL_RESPONSE_PAGE_SIZE, this.$el.find('.js-response-list'), true); }; DiscussionThreadView.prototype.renderResponseCountAndPagination = function(responseTotal) { @@ -286,47 +289,47 @@ responsesRemaining, showingResponsesText, self = this; if (this.isQuestion() && this.markedAnswers.length !== 0) { responseCountFormat = ngettext( - "{numResponses} other response", "{numResponses} other responses", responseTotal + '{numResponses} other response', '{numResponses} other responses', responseTotal ); } else { responseCountFormat = ngettext( - "{numResponses} response", "{numResponses} responses", responseTotal + '{numResponses} response', '{numResponses} responses', responseTotal ); } - this.$el.find(".response-count").text( + this.$el.find('.response-count').text( edx.StringUtils.interpolate(responseCountFormat, {numResponses: responseTotal}, true) ); - responsePagination = this.$el.find(".response-pagination"); + responsePagination = this.$el.find('.response-pagination'); responsePagination.empty(); if (responseTotal > 0) { responsesRemaining = responseTotal - this.responses.size(); if (responsesRemaining === 0) { - showingResponsesText = gettext("Showing all responses"); + showingResponsesText = gettext('Showing all responses'); } else { showingResponsesText = edx.StringUtils.interpolate( ngettext( - "Showing first response", "Showing first {numResponses} responses", + 'Showing first response', 'Showing first {numResponses} responses', this.responses.size() ), - { numResponses: this.responses.size() }, + {numResponses: this.responses.size()}, true ); } - responsePagination.append($("") - .addClass("response-display-count").text(showingResponsesText)); + responsePagination.append($('') + .addClass('response-display-count').text(showingResponsesText)); if (responsesRemaining > 0) { if (responsesRemaining < SUBSEQUENT_RESPONSE_PAGE_SIZE) { responseLimit = null; - buttonText = gettext("Load all responses"); + buttonText = gettext('Load all responses'); } else { responseLimit = SUBSEQUENT_RESPONSE_PAGE_SIZE; - buttonText = edx.StringUtils.interpolate(gettext("Load next {numResponses} responses"), { + buttonText = edx.StringUtils.interpolate(gettext('Load next {numResponses} responses'), { numResponses: responseLimit }, true); } - $loadMoreButton = $(" diff --git a/lms/templates/verify_student/webcam_photo.underscore b/lms/templates/verify_student/webcam_photo.underscore index c7ca5482f4..03969fea6f 100644 --- a/lms/templates/verify_student/webcam_photo.underscore +++ b/lms/templates/verify_student/webcam_photo.underscore @@ -10,7 +10,7 @@ diff --git a/openedx/core/djangoapps/course_groups/models.py b/openedx/core/djangoapps/course_groups/models.py index 14ce7a7fe8..3c3cad5791 100644 --- a/openedx/core/djangoapps/course_groups/models.py +++ b/openedx/core/djangoapps/course_groups/models.py @@ -90,7 +90,7 @@ class CohortMembership(models.Model): def save(self, *args, **kwargs): self.full_clean(validate_unique=False) - log.info("Saving CohortMembership for '%s' (id=%s) in '%s'", self.user.username, self.user.id, self.course_id) + log.info("Saving CohortMembership for user '%s' in '%s'", self.user.id, self.course_id) # Avoid infinite recursion if creating from get_or_create() call below. # This block also allows middleware to use CohortMembership.get_or_create without worrying about outer_atomic diff --git a/openedx/core/djangoapps/credit/api/eligibility.py b/openedx/core/djangoapps/credit/api/eligibility.py index e086b3eb74..8f37f8d29e 100644 --- a/openedx/core/djangoapps/credit/api/eligibility.py +++ b/openedx/core/djangoapps/credit/api/eligibility.py @@ -13,6 +13,9 @@ from openedx.core.djangoapps.credit.models import ( CreditCourse, CreditRequirement, CreditRequirementStatus, CreditEligibility, CreditRequest ) +from course_modes.models import CourseMode +from student.models import CourseEnrollment + # TODO: Cleanup this mess! ECOM-2908 log = logging.getLogger(__name__) @@ -196,7 +199,7 @@ def get_eligibilities_for_user(username, course_key=None): ] -def set_credit_requirement_status(username, course_key, req_namespace, req_name, status="satisfied", reason=None): +def set_credit_requirement_status(user, course_key, req_namespace, req_name, status="satisfied", reason=None): """ Update the user's requirement status. @@ -205,7 +208,7 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name, as eligible for credit in the course. Args: - username (str): Username of the user + user(User): User object to set credit requirement for. course_key (CourseKey): Identifier for the course associated with the requirement. req_namespace (str): Namespace of the requirement (e.g. "grade" or "reverification") req_name (str): Name of the requirement (e.g. "grade" or the location of the ICRV XBlock) @@ -225,22 +228,30 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name, ) """ + # Check whether user has credit eligible enrollment. + enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_key) + has_credit_eligible_enrollment = (CourseMode.is_credit_eligible_slug(enrollment_mode) and is_active) + + # Refuse to set status of requirement if the user enrollment is not credit eligible. + if not has_credit_eligible_enrollment: + return + # Do not allow students who have requested credit to change their eligibility - if CreditRequest.get_user_request_status(username, course_key): + if CreditRequest.get_user_request_status(user.username, course_key): log.info( u'Refusing to set status of requirement with namespace "%s" and name "%s" because the ' u'user "%s" has already requested credit for the course "%s".', - req_namespace, req_name, username, course_key + req_namespace, req_name, user.username, course_key ) return # Do not allow a student who has earned eligibility to un-earn eligibility - eligible_before_update = CreditEligibility.is_user_eligible_for_credit(course_key, username) + eligible_before_update = CreditEligibility.is_user_eligible_for_credit(course_key, user.username) if eligible_before_update and status == 'failed': log.info( u'Refusing to set status of requirement with namespace "%s" and name "%s" to "failed" because the ' u'user "%s" is already eligible for credit in the course "%s".', - req_namespace, req_name, username, course_key + req_namespace, req_name, user.username, course_key ) return @@ -269,22 +280,22 @@ def set_credit_requirement_status(username, course_key, req_namespace, req_name, u'because the requirement does not exist. ' u'The user "%s" should have had his/her status updated to "%s".' ), - unicode(course_key), req_namespace, req_name, username, status + unicode(course_key), req_namespace, req_name, user.username, status ) return # Update the requirement status CreditRequirementStatus.add_or_update_requirement_status( - username, req_to_update, status=status, reason=reason + user.username, req_to_update, status=status, reason=reason ) # If we're marking this requirement as "satisfied", there's a chance that the user has met all eligibility # requirements, and should be notified. However, if the user was already eligible, do not send another notification. if status == "satisfied" and not eligible_before_update: - is_eligible, eligibility_record_created = CreditEligibility.update_eligibility(reqs, username, course_key) + is_eligible, eligibility_record_created = CreditEligibility.update_eligibility(reqs, user.username, course_key) if eligibility_record_created and is_eligible: try: - send_credit_notifications(username, course_key) + send_credit_notifications(user.username, course_key) except Exception: # pylint: disable=broad-except log.error("Error sending email") diff --git a/openedx/core/djangoapps/credit/services.py b/openedx/core/djangoapps/credit/services.py index 77e7e7fa89..e38db41f22 100644 --- a/openedx/core/djangoapps/credit/services.py +++ b/openedx/core/djangoapps/credit/services.py @@ -155,7 +155,7 @@ class CreditService(object): return None api_set_credit_requirement_status( - user.username, + user, course_key, req_namespace, req_name, diff --git a/openedx/core/djangoapps/credit/signals.py b/openedx/core/djangoapps/credit/signals.py index 92973ad1f0..a239589600 100644 --- a/openedx/core/djangoapps/credit/signals.py +++ b/openedx/core/djangoapps/credit/signals.py @@ -53,12 +53,12 @@ def on_pre_publish(sender, course_key, **kwargs): # pylint: disable=unused-argu @receiver(GRADES_UPDATED) -def listen_for_grade_calculation(sender, username, grade_summary, course_key, deadline, **kwargs): # pylint: disable=unused-argument +def listen_for_grade_calculation(sender, user, grade_summary, course_key, deadline, **kwargs): # pylint: disable=unused-argument """Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade requirement status. Args: sender: None - username(string): user name + user(User): User Model object grade_summary(dict): Dict containing output from the course grader course_key(CourseKey): The key for the course deadline(datetime): Course end date or None @@ -70,7 +70,6 @@ def listen_for_grade_calculation(sender, username, grade_summary, course_key, de # This needs to be imported here to avoid a circular dependency # that can cause syncdb to fail. from openedx.core.djangoapps.credit import api - course_id = CourseKey.from_string(unicode(course_key)) is_credit = api.is_credit_course(course_id) if is_credit: @@ -113,5 +112,5 @@ def listen_for_grade_calculation(sender, username, grade_summary, course_key, de # time to do so. if status and reason: api.set_credit_requirement_status( - username, course_id, 'grade', 'grade', status=status, reason=reason + user, course_id, 'grade', 'grade', status=status, reason=reason ) diff --git a/openedx/core/djangoapps/credit/tests/test_api.py b/openedx/core/djangoapps/credit/tests/test_api.py index e90121d8db..b5e0386e04 100644 --- a/openedx/core/djangoapps/credit/tests/test_api.py +++ b/openedx/core/djangoapps/credit/tests/test_api.py @@ -36,6 +36,8 @@ from openedx.core.djangoapps.credit.models import ( CreditEligibility, CreditRequest ) +from course_modes.models import CourseMode +from student.models import CourseEnrollment from student.tests.factories import UserFactory from util.date_utils import from_timestamp from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -177,6 +179,19 @@ class CreditApiTestBase(ModuleStoreTestCase): return credit_course + def create_and_enroll_user(self, username, password, course_id=None, mode=CourseMode.VERIFIED): + """ Create and enroll the user in the given course's and given mode.""" + if course_id is None: + course_id = self.course_key + + user = UserFactory.create(username=username, password=password) + self.enroll(user, course_id, mode) + return user + + def enroll(self, user, course_id, mode): + """Enroll user in given course and mode""" + return CourseEnrollment.enroll(user, course_id, mode=mode) + def _mock_ecommerce_courses_api(self, course_key, body, status=200): """ Mock GET requests to the ecommerce course API endpoint. """ httpretty.reset() @@ -330,14 +345,54 @@ class CreditRequirementApiTests(CreditApiTestBase): def test_is_user_eligible_for_credit(self): credit_course = self.add_credit_course() CreditEligibility.objects.create( - course=credit_course, username="staff" + course=credit_course, username=self.user.username ) - is_eligible = api.is_user_eligible_for_credit('staff', credit_course.course_key) + is_eligible = api.is_user_eligible_for_credit(self.user.username, credit_course.course_key) self.assertTrue(is_eligible) is_eligible = api.is_user_eligible_for_credit('abc', credit_course.course_key) self.assertFalse(is_eligible) + @ddt.data( + CourseMode.AUDIT, + CourseMode.HONOR, + CourseMode.CREDIT_MODE + ) + def test_user_eligibility_with_non_verified_enrollment(self, mode): + """ + Tests that user do not become credit eligible even after meeting the credit requirements. + + User can not become credit eligible if he does not has credit eligible enrollment in the course. + """ + self.add_credit_course() + + # Enroll user and verify his enrollment. + self.enroll(self.user, self.course_key, mode) + self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key)) + self.assertTrue(CourseEnrollment.enrollment_mode_for_user(self.user, self.course_key), (mode, True)) + + requirements = [ + { + "namespace": "grade", + "name": "grade", + "display_name": "Grade", + "criteria": { + "min_grade": 0.6 + }, + } + ] + # Set & verify course credit requirements. + api.set_credit_requirements(self.course_key, requirements) + requirements = api.get_credit_requirements(self.course_key) + self.assertEqual(len(requirements), 1) + + # Set the requirement to "satisfied" and check that they are not set for non-credit eligible enrollment. + api.set_credit_requirement_status(self.user, self.course_key, "grade", "grade", status='satisfied') + self.assert_grade_requirement_status(None, 0) + + # Verify user is not eligible for credit. + self.assertFalse(api.is_user_eligible_for_credit(self.user.username, self.course_key)) + def test_eligibility_expired(self): # Configure a credit eligibility that expired yesterday credit_course = self.add_credit_course() @@ -376,14 +431,23 @@ class CreditRequirementApiTests(CreditApiTestBase): def assert_grade_requirement_status(self, expected_status, expected_order): """ Assert the status and order of the grade requirement. """ - req_status = api.get_credit_requirement_status(self.course_key, 'staff', namespace="grade", name="grade") + req_status = api.get_credit_requirement_status(self.course_key, self.user, namespace="grade", name="grade") self.assertEqual(req_status[0]["status"], expected_status) self.assertEqual(req_status[0]["order"], expected_order) return req_status - def test_set_credit_requirement_status(self): - username = "staff" + @ddt.data( + *CourseMode.CREDIT_ELIGIBLE_MODES + ) + def test_set_credit_requirement_status(self, mode): + username = self.user.username credit_course = self.add_credit_course() + + # Enroll user and verify his enrollment. + self.enroll(self.user, self.course_key, mode) + self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course_key)) + self.assertTrue(CourseEnrollment.enrollment_mode_for_user(self.user, self.course_key), (mode, True)) + requirements = [ { "namespace": "grade", @@ -400,7 +464,6 @@ class CreditRequirementApiTests(CreditApiTestBase): "criteria": {}, } ] - api.set_credit_requirements(self.course_key, requirements) course_requirements = api.get_credit_requirements(self.course_key) self.assertEqual(len(course_requirements), 2) @@ -414,19 +477,19 @@ class CreditRequirementApiTests(CreditApiTestBase): provider=CreditProvider.objects.first(), username=username, ) - api.set_credit_requirement_status(username, self.course_key, "grade", "grade") + api.set_credit_requirement_status(self.user, self.course_key, "grade", "grade") self.assert_grade_requirement_status(None, 0) credit_request.delete() # Set the requirement to "satisfied" and check that it's actually set - api.set_credit_requirement_status(username, self.course_key, "grade", "grade") + api.set_credit_requirement_status(self.user, self.course_key, "grade", "grade") self.assert_grade_requirement_status('satisfied', 0) # Set the requirement to "failed" and check that it's actually set - api.set_credit_requirement_status(username, self.course_key, "grade", "grade", status="failed") + api.set_credit_requirement_status(self.user, self.course_key, "grade", "grade", status="failed") self.assert_grade_requirement_status('failed', 0) - req_status = api.get_credit_requirement_status(self.course_key, "staff") + req_status = api.get_credit_requirement_status(self.course_key, username) self.assertEqual(req_status[0]["status"], "failed") self.assertEqual(req_status[0]["order"], 0) @@ -436,7 +499,7 @@ class CreditRequirementApiTests(CreditApiTestBase): # Set the requirement to "declined" and check that it's actually set api.set_credit_requirement_status( - username, self.course_key, + self.user, self.course_key, "reverification", "i4x://edX/DemoX/edx-reverification-block/assessment_uuid", status="declined" @@ -449,8 +512,13 @@ class CreditRequirementApiTests(CreditApiTestBase): ) self.assertEqual(req_status[0]["status"], "declined") - def test_remove_credit_requirement_status(self): + @ddt.data( + *CourseMode.CREDIT_ELIGIBLE_MODES + ) + def test_remove_credit_requirement_status(self, mode): self.add_credit_course() + self.enroll(self.user, self.course_key, mode) + username = self.user.username requirements = [ { "namespace": "grade", @@ -467,27 +535,26 @@ class CreditRequirementApiTests(CreditApiTestBase): "criteria": {}, } ] - api.set_credit_requirements(self.course_key, requirements) course_requirements = api.get_credit_requirements(self.course_key) self.assertEqual(len(course_requirements), 2) # before setting credit_requirement_status - api.remove_credit_requirement_status("staff", self.course_key, "grade", "grade") - req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") + api.remove_credit_requirement_status(username, self.course_key, "grade", "grade") + req_status = api.get_credit_requirement_status(self.course_key, username, namespace="grade", name="grade") self.assertIsNone(req_status[0]["status"]) self.assertIsNone(req_status[0]["status_date"]) self.assertIsNone(req_status[0]["reason"]) # Set the requirement to "satisfied" and check that it's actually set - api.set_credit_requirement_status("staff", self.course_key, "grade", "grade") - req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") + api.set_credit_requirement_status(self.user, self.course_key, "grade", "grade") + req_status = api.get_credit_requirement_status(self.course_key, username, namespace="grade", name="grade") self.assertEqual(len(req_status), 1) self.assertEqual(req_status[0]["status"], "satisfied") # remove the credit requirement status and check that it's actually removed - api.remove_credit_requirement_status("staff", self.course_key, "grade", "grade") - req_status = api.get_credit_requirement_status(self.course_key, "staff", namespace="grade", name="grade") + api.remove_credit_requirement_status(self.user.username, self.course_key, "grade", "grade") + req_status = api.get_credit_requirement_status(self.course_key, username, namespace="grade", name="grade") self.assertIsNone(req_status[0]["status"]) self.assertIsNone(req_status[0]["status_date"]) self.assertIsNone(req_status[0]["reason"]) @@ -522,6 +589,7 @@ class CreditRequirementApiTests(CreditApiTestBase): # Configure a course with two credit requirements self.add_credit_course() + user = self.create_and_enroll_user(username=self.USER_INFO['username'], password=self.USER_INFO['password']) CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course') requirements = [ @@ -542,31 +610,29 @@ class CreditRequirementApiTests(CreditApiTestBase): ] api.set_credit_requirements(self.course_key, requirements) - user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password']) - # Satisfy one of the requirements, but not the other - with self.assertNumQueries(12): + with self.assertNumQueries(13): api.set_credit_requirement_status( - user.username, + user, self.course_key, requirements[0]["namespace"], requirements[0]["name"] ) # The user should not be eligible (because only one requirement is satisfied) - self.assertFalse(api.is_user_eligible_for_credit("bob", self.course_key)) + self.assertFalse(api.is_user_eligible_for_credit(user.username, self.course_key)) # Satisfy the other requirement - with self.assertNumQueries(21): + with self.assertNumQueries(22): api.set_credit_requirement_status( - "bob", + user, self.course_key, requirements[1]["namespace"], requirements[1]["name"] ) # Now the user should be eligible - self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key)) + self.assertTrue(api.is_user_eligible_for_credit(user.username, self.course_key)) # Credit eligibility email should be sent self.assertEqual(len(mail.outbox), 1) @@ -611,9 +677,9 @@ class CreditRequirementApiTests(CreditApiTestBase): # Delete the eligibility entries and satisfy the user's eligibility # requirement again to trigger eligibility notification CreditEligibility.objects.all().delete() - with self.assertNumQueries(16): + with self.assertNumQueries(17): api.set_credit_requirement_status( - "bob", + user, self.course_key, requirements[1]["namespace"], requirements[1]["name"] @@ -629,26 +695,27 @@ class CreditRequirementApiTests(CreditApiTestBase): # The user should remain eligible even if the requirement status is later changed api.set_credit_requirement_status( - "bob", + user, self.course_key, requirements[0]["namespace"], requirements[0]["name"], status="failed" ) - self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key)) + self.assertTrue(api.is_user_eligible_for_credit(user.username, self.course_key)) def test_set_credit_requirement_status_req_not_configured(self): # Configure a credit course with no requirements + username = self.user.username self.add_credit_course() # A user satisfies a requirement. This could potentially # happen if there's a lag when the requirements are updated # after the course is published. - api.set_credit_requirement_status("bob", self.course_key, "grade", "grade") + api.set_credit_requirement_status(self.user, self.course_key, "grade", "grade") # Since the requirement hasn't been published yet, it won't show # up in the list of requirements. - req_status = api.get_credit_requirement_status(self.course_key, "bob", namespace="grade", name="grade") + req_status = api.get_credit_requirement_status(self.course_key, username, namespace="grade", name="grade") self.assertEqual(req_status, []) # Now add the requirements, simulating what happens when a course is published. @@ -672,7 +739,7 @@ class CreditRequirementApiTests(CreditApiTestBase): # The user should not have satisfied the requirements, since they weren't # in effect when the user completed the requirement - req_status = api.get_credit_requirement_status(self.course_key, "bob") + req_status = api.get_credit_requirement_status(self.course_key, username) self.assertEqual(len(req_status), 2) self.assertEqual(req_status[0]["status"], None) self.assertEqual(req_status[0]["status"], None) @@ -680,7 +747,7 @@ class CreditRequirementApiTests(CreditApiTestBase): # The user should *not* have satisfied the reverification requirement req_status = api.get_credit_requirement_status( self.course_key, - "bob", + username, namespace=requirements[1]["namespace"], name=requirements[1]["name"] ) @@ -713,6 +780,7 @@ class CreditRequirementApiTests(CreditApiTestBase): """ # Configure a course with two credit requirements self.add_credit_course() + user = self.create_and_enroll_user(username=self.USER_INFO['username'], password=self.USER_INFO['password']) CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course') requirements = [ { @@ -732,11 +800,9 @@ class CreditRequirementApiTests(CreditApiTestBase): ] api.set_credit_requirements(self.course_key, requirements) - user = UserFactory.create(username=self.USER_INFO['username'], password=self.USER_INFO['password']) - # Satisfy one of the requirements, but not the other api.set_credit_requirement_status( - user.username, + user, self.course_key, requirements[0]["namespace"], requirements[0]["name"] @@ -745,13 +811,13 @@ class CreditRequirementApiTests(CreditApiTestBase): with mock.patch('openedx.core.djangoapps.credit.email_utils.get_credit_provider_display_names') as mock_method: mock_method.return_value = providers_list api.set_credit_requirement_status( - "bob", + user, self.course_key, requirements[1]["namespace"], requirements[1]["name"] ) # Now the user should be eligible - self.assertTrue(api.is_user_eligible_for_credit("bob", self.course_key)) + self.assertTrue(api.is_user_eligible_for_credit(user.username, self.course_key)) # Credit eligibility email should be sent self.assertEqual(len(mail.outbox), 1) diff --git a/openedx/core/djangoapps/credit/tests/test_services.py b/openedx/core/djangoapps/credit/tests/test_services.py index 0575261a2a..60b0f83521 100644 --- a/openedx/core/djangoapps/credit/tests/test_services.py +++ b/openedx/core/djangoapps/credit/tests/test_services.py @@ -2,7 +2,9 @@ Tests for the Credit xBlock service """ +import ddt from nose.plugins.attrib import attr +from course_modes.models import CourseMode from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -15,6 +17,7 @@ from student.models import CourseEnrollment, UserProfile @attr(shard=2) +@ddt.ddt class CreditServiceTests(ModuleStoreTestCase): """ Tests for the Credit xBlock service @@ -28,14 +31,14 @@ class CreditServiceTests(ModuleStoreTestCase): self.credit_course = CreditCourse.objects.create(course_key=self.course.id, enabled=True) self.profile = UserProfile.objects.create(user_id=self.user.id, name='Foo Bar') - def enroll(self, course_id=None): + def enroll(self, course_id=None, mode=CourseMode.VERIFIED): """ - Enroll the test user in the given course's honor mode, or the test - course if not provided. + Enroll the test user in the given course's mode. Use course/mode if they are + provided. """ if course_id is None: course_id = self.course.id - return CourseEnrollment.enroll(self.user, course_id, mode='honor') + return CourseEnrollment.enroll(self.user, course_id, mode=mode) def test_user_not_found(self): """ @@ -127,7 +130,7 @@ class CreditServiceTests(ModuleStoreTestCase): self.assertIsNotNone(credit_state) self.assertTrue(credit_state['is_credit_course']) - self.assertEqual(credit_state['enrollment_mode'], 'honor') + self.assertEqual(credit_state['enrollment_mode'], 'verified') self.assertEqual(credit_state['profile_fullname'], 'Foo Bar') self.assertEqual(len(credit_state['credit_requirement_status']), 1) self.assertEqual(credit_state['credit_requirement_status'][0]['name'], 'grade') @@ -286,6 +289,48 @@ class CreditServiceTests(ModuleStoreTestCase): self.assertFalse(credit_state['is_credit_course']) self.assertEqual(len(credit_state['credit_requirement_status']), 0) + @ddt.data( + CourseMode.AUDIT, + CourseMode.HONOR, + CourseMode.CREDIT_MODE + ) + def test_set_status_non_verified_enrollment(self, mode): + """ + Test that we can still try to update a credit status but return quickly if + user has non-credit eligible enrollment. + """ + self.enroll(mode=mode) + + # set course requirements + set_credit_requirements( + self.course.id, + [ + { + "namespace": "grade", + "name": "grade", + "display_name": "Grade", + "criteria": { + "min_grade": 0.8 + }, + }, + ] + ) + + # this should be a no-op + self.service.set_credit_requirement_status( + self.user.id, + self.course.id, + 'grade', + 'grade' + ) + # Verify credit requirement status for user in the course should be None. + credit_state = self.service.get_credit_state(self.user.id, self.course.id) + self.assertIsNotNone(credit_state) + self.assertEqual(credit_state['enrollment_mode'], mode) + self.assertEqual(len(credit_state['credit_requirement_status']), 1) + self.assertIsNone(credit_state['credit_requirement_status'][0]['status']) + self.assertIsNone(credit_state['credit_requirement_status'][0]['status_date']) + def test_bad_user(self): """ Try setting requirements status with a bad user_id @@ -348,7 +393,7 @@ class CreditServiceTests(ModuleStoreTestCase): credit_state = self.service.get_credit_state(self.user.id, unicode(self.course.id)) self.assertIsNotNone(credit_state) - self.assertEqual(credit_state['enrollment_mode'], 'honor') + self.assertEqual(credit_state['enrollment_mode'], 'verified') self.assertEqual(credit_state['profile_fullname'], 'Foo Bar') self.assertEqual(len(credit_state['credit_requirement_status']), 1) self.assertEqual(credit_state['credit_requirement_status'][0]['name'], 'grade') diff --git a/openedx/core/djangoapps/credit/tests/test_signals.py b/openedx/core/djangoapps/credit/tests/test_signals.py index 376b8d8769..f3d55529a2 100644 --- a/openedx/core/djangoapps/credit/tests/test_signals.py +++ b/openedx/core/djangoapps/credit/tests/test_signals.py @@ -10,6 +10,8 @@ from unittest import skipUnless from django.conf import settings from django.test.client import RequestFactory from nose.plugins.attrib import attr +from course_modes.models import CourseMode +from student.models import CourseEnrollment from student.tests.factories import UserFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -66,9 +68,12 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): # Add a single credit requirement (final grade) set_credit_requirements(self.course.id, requirements) + # Enroll user in verified mode. + self.enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.VERIFIED) + def assert_requirement_status(self, grade, due_date, expected_status): """ Verify the user's credit requirement status is as expected after simulating a grading calculation. """ - listen_for_grade_calculation(None, self.user.username, {'percent': grade}, self.course.id, due_date) + listen_for_grade_calculation(None, self.user, {'percent': grade}, self.course.id, due_date) req_status = get_credit_requirement_status(self.course.id, self.request.user.username, 'grade', 'grade') self.assertEqual(req_status[0]['status'], expected_status) @@ -109,3 +114,13 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase): def test_min_grade_requirement_failed_grade_expired_deadline(self): """Test with failed grades and deadline expire""" self.assert_requirement_status(0.22, self.EXPIRED_DUE_DATE, 'failed') + + @ddt.data( + CourseMode.AUDIT, + CourseMode.HONOR, + CourseMode.CREDIT_MODE + ) + def test_requirement_failed_for_non_verified_enrollment(self, mode): + """Test with valid grades submitted before deadline with non-verified enrollment.""" + self.enrollment.update_enrollment(mode, True) + self.assert_requirement_status(0.8, self.VALID_DUE_DATE, None) diff --git a/openedx/core/djangoapps/programs/models.py b/openedx/core/djangoapps/programs/models.py index d903ab6578..b6eec9ebe4 100644 --- a/openedx/core/djangoapps/programs/models.py +++ b/openedx/core/djangoapps/programs/models.py @@ -7,6 +7,7 @@ from django.db import models from config_models.models import ConfigurationModel +# TODO: To be simplified as part of ECOM-5136. class ProgramsApiConfig(ConfigurationModel): """ Manages configuration for connecting to the Programs service and using its @@ -29,7 +30,6 @@ class ProgramsApiConfig(ConfigurationModel): ) ) - # TODO: The property below is obsolete. Delete at the earliest safe moment. See ECOM-4995 authoring_app_js_path = models.CharField( verbose_name=_("Path to authoring app's JS"), max_length=255, @@ -39,7 +39,6 @@ class ProgramsApiConfig(ConfigurationModel): ) ) - # TODO: The property below is obsolete. Delete at the earliest safe moment. See ECOM-4995 authoring_app_css_path = models.CharField( verbose_name=_("Path to authoring app's CSS"), max_length=255, @@ -81,7 +80,6 @@ class ProgramsApiConfig(ConfigurationModel): ) ) - # TODO: Remove unused field. xseries_ad_enabled = models.BooleanField( verbose_name=_("Do we want to show xseries program advertising"), default=False @@ -116,14 +114,6 @@ class ProgramsApiConfig(ConfigurationModel): """Whether responses from the Programs API will be cached.""" return self.cache_ttl > 0 - @property - def is_student_dashboard_enabled(self): - """ - Indicates whether LMS dashboard functionality related to Programs should - be enabled or not. - """ - return self.enabled and self.enable_student_dashboard - @property def is_studio_tab_enabled(self): """ diff --git a/openedx/core/djangoapps/programs/tests/factories.py b/openedx/core/djangoapps/programs/tests/factories.py index f4e52e18e2..23484c0dd7 100644 --- a/openedx/core/djangoapps/programs/tests/factories.py +++ b/openedx/core/djangoapps/programs/tests/factories.py @@ -14,7 +14,7 @@ class Program(factory.Factory): name = FuzzyText(prefix='Program ') subtitle = FuzzyText(prefix='Subtitle ') category = 'FooBar' - status = 'unpublished' + status = 'active' marketing_slug = FuzzyText(prefix='slug_') organizations = [] course_codes = [] diff --git a/openedx/core/djangoapps/programs/tests/mixins.py b/openedx/core/djangoapps/programs/tests/mixins.py index e13f92080e..e393167b8f 100644 --- a/openedx/core/djangoapps/programs/tests/mixins.py +++ b/openedx/core/djangoapps/programs/tests/mixins.py @@ -16,7 +16,6 @@ class ProgramsApiConfigMixin(object): 'internal_service_url': 'http://internal.programs.org/', 'public_service_url': 'http://public.programs.org/', 'cache_ttl': 0, - 'enable_student_dashboard': True, 'enable_studio_tab': True, 'enable_certification': True, 'program_listing_enabled': True, diff --git a/openedx/core/djangoapps/programs/tests/test_models.py b/openedx/core/djangoapps/programs/tests/test_models.py index 5c318c2cdc..a0b7aba446 100644 --- a/openedx/core/djangoapps/programs/tests/test_models.py +++ b/openedx/core/djangoapps/programs/tests/test_models.py @@ -36,20 +36,6 @@ class TestProgramsApiConfig(ProgramsApiConfigMixin, TestCase): programs_config = self.create_programs_config(cache_ttl=cache_ttl) self.assertEqual(programs_config.is_cache_enabled, is_cache_enabled) - def test_is_student_dashboard_enabled(self, _mock_cache): - """ - Verify that the property controlling display on the student dashboard is only True - when configuration is enabled and all required configuration is provided. - """ - programs_config = self.create_programs_config(enabled=False) - self.assertFalse(programs_config.is_student_dashboard_enabled) - - programs_config = self.create_programs_config(enable_student_dashboard=False) - self.assertFalse(programs_config.is_student_dashboard_enabled) - - programs_config = self.create_programs_config() - self.assertTrue(programs_config.is_student_dashboard_enabled) - def test_is_studio_tab_enabled(self, _mock_cache): """ Verify that the property controlling display of the Studio tab is only True diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 15049b5c07..b482cbc1b3 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -12,9 +12,11 @@ from django.core.urlresolvers import reverse from django.test import TestCase from django.test.utils import override_settings from django.utils import timezone +from django.utils.text import slugify import httpretty import mock from nose.plugins.attrib import attr +from opaque_keys.edx.keys import CourseKey from edx_oauth2_provider.tests.factories import ClientFactory from provider.constants import CONFIDENTIAL @@ -141,36 +143,6 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, Credential actual = utils.get_programs(self.user) self.assertEqual(actual, []) - def test_get_programs_for_dashboard(self): - """Verify programs data can be retrieved and parsed correctly.""" - self.create_programs_config() - self.mock_programs_api() - - actual = utils.get_programs_for_dashboard(self.user, self.COURSE_KEYS) - expected = {} - for program in self.PROGRAMS_API_RESPONSE['results']: - for course_code in program['course_codes']: - for run in course_code['run_modes']: - course_key = run['course_key'] - expected.setdefault(course_key, []).append(program) - - self.assertEqual(actual, expected) - - def test_get_programs_for_dashboard_dashboard_display_disabled(self): - """Verify behavior when student dashboard display is disabled.""" - self.create_programs_config(enable_student_dashboard=False) - - actual = utils.get_programs_for_dashboard(self.user, self.COURSE_KEYS) - self.assertEqual(actual, {}) - - def test_get_programs_for_dashboard_no_data(self): - """Verify behavior when no programs data is found for the user.""" - self.create_programs_config() - self.mock_programs_api(data={'results': []}) - - actual = utils.get_programs_for_dashboard(self.user, self.COURSE_KEYS) - self.assertEqual(actual, {}) - def test_get_program_for_certificates(self): """Verify programs data can be retrieved and parsed correctly for certificates.""" self.create_programs_config() @@ -218,6 +190,78 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin, Credential self.assertEqual(actual, []) +@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +class GetProgramsByRunTests(TestCase): + """Tests verifying that programs are inverted correctly.""" + maxDiff = None + + @classmethod + def setUpClass(cls): + super(GetProgramsByRunTests, cls).setUpClass() + + cls.user = UserFactory() + + course_keys = [ + CourseKey.from_string('some/course/run'), + CourseKey.from_string('some/other/run'), + ] + + cls.enrollments = [CourseEnrollmentFactory(user=cls.user, course_id=c) for c in course_keys] + cls.course_ids = [unicode(c) for c in course_keys] + + organization = factories.Organization() + joint_programs = sorted([ + factories.Program( + organizations=[organization], + course_codes=[ + factories.CourseCode(run_modes=[ + factories.RunMode(course_key=cls.course_ids[0]), + ]), + ] + ) for __ in range(2) + ], key=lambda p: p['name']) + + cls.programs = joint_programs + [ + factories.Program( + organizations=[organization], + course_codes=[ + factories.CourseCode(run_modes=[ + factories.RunMode(course_key=cls.course_ids[1]), + ]), + ] + ), + factories.Program( + organizations=[organization], + course_codes=[ + factories.CourseCode(run_modes=[ + factories.RunMode(course_key='yet/another/run'), + ]), + ] + ), + ] + + def test_get_programs_by_run(self): + """Verify that programs are organized by run ID.""" + programs_by_run, course_ids = utils.get_programs_by_run(self.programs, self.enrollments) + + self.assertEqual(programs_by_run[self.course_ids[0]], self.programs[:2]) + self.assertEqual(programs_by_run[self.course_ids[1]], self.programs[2:3]) + + self.assertEqual(course_ids, self.course_ids) + + def test_no_programs(self): + """Verify that the utility can cope with missing programs data.""" + programs_by_run, course_ids = utils.get_programs_by_run([], self.enrollments) + self.assertEqual(programs_by_run, {}) + self.assertEqual(course_ids, self.course_ids) + + def test_no_enrollments(self): + """Verify that the utility can cope with missing enrollment data.""" + programs_by_run, course_ids = utils.get_programs_by_run(self.programs, []) + self.assertEqual(programs_by_run, {}) + self.assertEqual(course_ids, []) + + @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class GetCompletedCoursesTestCase(TestCase): """ @@ -297,6 +341,14 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): """Construct a list containing the display names of the indicated course codes.""" return [program['course_codes'][cc]['display_name'] for cc in course_codes] + def _attach_detail_url(self, programs): + """Add expected detail URLs to a list of program dicts.""" + for program in programs: + base = reverse('program_details_view', kwargs={'program_id': program['id']}).rstrip('/') + slug = slugify(program['name']) + + program['detail_url'] = '{base}/{slug}'.format(base=base, slug=slug) + def test_no_enrollments(self): """Verify behavior when programs exist, but no relevant enrollments do.""" data = [ @@ -311,7 +363,7 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): meter = utils.ProgramProgressMeter(self.user) - self.assertEqual(meter.engaged_programs, []) + self.assertEqual(meter.engaged_programs(), []) self._assert_progress(meter) self.assertEqual(meter.completed_programs, []) @@ -322,7 +374,7 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): self._create_enrollments('org/course/run') meter = utils.ProgramProgressMeter(self.user) - self.assertEqual(meter.engaged_programs, []) + self.assertEqual(meter.engaged_programs(), []) self._assert_progress(meter) self.assertEqual(meter.completed_programs, []) @@ -353,8 +405,9 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): self._create_enrollments(course_id) meter = utils.ProgramProgressMeter(self.user) + self._attach_detail_url(data) program = data[0] - self.assertEqual(meter.engaged_programs, [program]) + self.assertEqual(meter.engaged_programs(), [program]) self._assert_progress( meter, factories.Progress( @@ -399,8 +452,9 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): self._create_enrollments(second_course_id, first_course_id) meter = utils.ProgramProgressMeter(self.user) + self._attach_detail_url(data) programs = data[:2] - self.assertEqual(meter.engaged_programs, programs) + self.assertEqual(meter.engaged_programs(), programs) self._assert_progress( meter, factories.Progress(id=programs[0]['id'], in_progress=self._extract_names(programs[0], 0)), @@ -414,7 +468,8 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): appearing in multiple programs. """ shared_course_id, solo_course_id = 'org/shared-course/run', 'org/solo-course/run' - data = [ + + joint_programs = sorted([ factories.Program( organizations=[factories.Organization()], course_codes=[ @@ -422,15 +477,10 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): factories.RunMode(course_key=shared_course_id), ]), ] - ), - factories.Program( - organizations=[factories.Organization()], - course_codes=[ - factories.CourseCode(run_modes=[ - factories.RunMode(course_key=shared_course_id), - ]), - ] - ), + ) for __ in range(2) + ], key=lambda p: p['name']) + + data = joint_programs + [ factories.Program( organizations=[factories.Organization()], course_codes=[ @@ -446,14 +496,16 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): ] ), ] + self._mock_programs_api(data) # Enrollment for the shared course ID created last (most recently). self._create_enrollments(solo_course_id, shared_course_id) meter = utils.ProgramProgressMeter(self.user) + self._attach_detail_url(data) programs = data[:3] - self.assertEqual(meter.engaged_programs, programs) + self.assertEqual(meter.engaged_programs(), programs) self._assert_progress( meter, factories.Progress(id=programs[0]['id'], in_progress=self._extract_names(programs[0], 0)), diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index 8ec2173c7c..e790b7a273 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -2,10 +2,11 @@ """Helper functions for working with Programs.""" import datetime import logging +from urlparse import urljoin +from django.conf import settings from django.core.urlresolvers import reverse from django.utils import timezone -from django.utils.functional import cached_property from django.utils.text import slugify from opaque_keys.edx.keys import CourseKey import pytz @@ -52,63 +53,6 @@ def get_programs(user, program_id=None): return get_edx_api_data(programs_config, user, 'programs', resource_id=program_id, cache_key=cache_key) -def flatten_programs(programs, course_ids): - """Flatten the result returned by the Programs API. - - Arguments: - programs (list): Serialized programs - course_ids (list): Course IDs to key on. - - Returns: - dict, programs keyed by course ID - """ - flattened = {} - - for program in programs: - try: - for course_code in program['course_codes']: - for run in course_code['run_modes']: - run_id = run['course_key'] - if run_id in course_ids: - flattened.setdefault(run_id, []).append(program) - except KeyError: - log.exception('Unable to parse Programs API response: %r', program) - - return flattened - - -def get_programs_for_dashboard(user, course_keys): - """Build a dictionary of programs, keyed by course. - - Given a user and an iterable of course keys, find all the programs relevant - to the user's dashboard and return them in a dictionary keyed by course key. - - Arguments: - user (User): The user to authenticate as when requesting programs. - course_keys (list): List of course keys representing the courses in which - the given user has active enrollments. - - Returns: - dict, containing programs keyed by course. Empty if programs cannot be retrieved. - """ - programs_config = ProgramsApiConfig.current() - course_programs = {} - - if not programs_config.is_student_dashboard_enabled: - log.debug('Display of programs on the student dashboard is disabled.') - return course_programs - - programs = get_programs(user) - if not programs: - log.debug('No programs found for the user with ID %d.', user.id) - return course_programs - - course_ids = [unicode(c) for c in course_keys] - course_programs = flatten_programs(programs, course_ids) - - return course_programs - - def get_programs_for_credentials(user, programs_credentials): """ Given a user and an iterable of credentials, get corresponding programs data and return it as a list of dictionaries. @@ -137,24 +81,71 @@ def get_programs_for_credentials(user, programs_credentials): return certificate_programs -def get_program_detail_url(program, marketing_root): - """Construct the URL to be used when linking to program details. +def get_programs_by_run(programs, enrollments): + """Intersect programs and enrollments. + + Builds a dictionary of program dict lists keyed by course ID. The resulting dictionary + is suitable for use in applications where programs must be filtered by the course + runs they contain (e.g., student dashboard). Arguments: - program (dict): Representation of a program. - marketing_root (str): Root URL used to build links to program marketing pages. + programs (list): Containing dictionaries representing programs. + enrollments (list): Enrollments from which course IDs to key on can be extracted. Returns: - str, a link to program details + tuple, dict of programs keyed by course ID and list of course IDs themselves """ - if ProgramsApiConfig.current().show_program_details: - base = reverse('program_details_view', kwargs={'program_id': program['id']}).rstrip('/') - slug = slugify(program['name']) - else: - base = marketing_root.rstrip('/') - slug = program['marketing_slug'] + programs_by_run = {} + # enrollment.course_id is really a course key (╯ಠ_ಠ)╯︵ ┻━┻ + course_ids = [unicode(e.course_id) for e in enrollments] - return '{base}/{slug}'.format(base=base, slug=slug) + for program in programs: + for course_code in program['course_codes']: + for run in course_code['run_modes']: + run_id = run['course_key'] + if run_id in course_ids: + program_list = programs_by_run.setdefault(run_id, list()) + if program not in program_list: + program_list.append(program) + + # Sort programs by name for consistent presentation. + for program_list in programs_by_run.itervalues(): + program_list.sort(key=lambda p: p['name']) + + return programs_by_run, course_ids + + +def get_program_marketing_url(programs_config): + """Build a URL to be used when linking to program details on a marketing site.""" + return urljoin(settings.MKTG_URLS.get('ROOT'), programs_config.marketing_path).rstrip('/') + + +def attach_program_detail_url(programs): + """Extend program representations by attaching a URL to be used when linking to program details. + + Facilitates the building of context to be passed to templates containing program data. + + Arguments: + programs (list): Containing dicts representing programs. + + Returns: + list, containing extended program dicts + """ + programs_config = ProgramsApiConfig.current() + marketing_url = get_program_marketing_url(programs_config) + + for program in programs: + if programs_config.show_program_details: + base = reverse('program_details_view', kwargs={'program_id': program['id']}).rstrip('/') + slug = slugify(program['name']) + else: + # TODO: Remove. Learners should always be sent to the LMS' program details page. + base = marketing_url + slug = program['marketing_slug'] + + program['detail_url'] = '{base}/{slug}'.format(base=base, slug=slug) + + return programs def get_completed_courses(student): @@ -182,35 +173,40 @@ class ProgramProgressMeter(object): Arguments: user (User): The user for which to find programs. + + Keyword Arguments: + enrollments (list): List of the user's enrollments. """ - def __init__(self, user): + def __init__(self, user, enrollments=None): self.user = user + self.enrollments = enrollments self.course_ids = None + self.course_certs = None - self.programs = get_programs(self.user) - self.course_certs = get_completed_courses(self.user) + self.programs = attach_program_detail_url(get_programs(self.user)) - @cached_property - def engaged_programs(self): + def engaged_programs(self, by_run=False): """Derive a list of programs in which the given user is engaged. Returns: - list of program dicts, ordered by most recent enrollment. + list of program dicts, ordered by most recent enrollment, + or dict of programs, keyed by course ID. """ - enrollments = CourseEnrollment.enrollments_for_user(self.user) - enrollments = sorted(enrollments, key=lambda e: e.created, reverse=True) - # enrollment.course_id is really a course key ಠ_ಠ - self.course_ids = [unicode(e.course_id) for e in enrollments] + self.enrollments = self.enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) + self.enrollments.sort(key=lambda e: e.created, reverse=True) - flattened = flatten_programs(self.programs, self.course_ids) + programs_by_run, self.course_ids = get_programs_by_run(self.programs, self.enrollments) - engaged_programs = [] + if by_run: + return programs_by_run + + programs = [] for course_id in self.course_ids: - for program in flattened.get(course_id, []): - if program not in engaged_programs: - engaged_programs.append(program) + for program in programs_by_run.get(course_id, []): + if program not in programs: + programs.append(program) - return engaged_programs + return programs @property def progress(self): @@ -221,7 +217,7 @@ class ProgramProgressMeter(object): towards completing a program. """ progress = [] - for program in self.engaged_programs: + for program in self.engaged_programs(): completed, in_progress, not_started = [], [], [] for course_code in program['course_codes']: @@ -277,6 +273,8 @@ class ProgramProgressMeter(object): Returns: bool, whether the course code is complete. """ + self.course_certs = self.course_certs or get_completed_courses(self.user) + return any(self._parse(run_mode) in self.course_certs for run_mode in course_code['run_modes']) def _is_course_code_in_progress(self, course_code): diff --git a/openedx/core/djangoapps/signals/signals.py b/openedx/core/djangoapps/signals/signals.py index a953ee1c3e..cfbf23969b 100644 --- a/openedx/core/djangoapps/signals/signals.py +++ b/openedx/core/djangoapps/signals/signals.py @@ -6,7 +6,7 @@ from django.dispatch import Signal # Signal that fires when a user is graded (in lms/grades/course_grades.py) -GRADES_UPDATED = Signal(providing_args=["username", "grade_summary", "course_key", "deadline"]) +GRADES_UPDATED = Signal(providing_args=["user", "grade_summary", "course_key", "deadline"]) # Signal that fires when a user is awarded a certificate in a course (in the certificates django app) # TODO: runtime coupling between apps will be reduced if this event is changed to carry a username diff --git a/openedx/core/djangoapps/user_api/preferences/api.py b/openedx/core/djangoapps/user_api/preferences/api.py index cc18bb51df..12704eb9a8 100644 --- a/openedx/core/djangoapps/user_api/preferences/api.py +++ b/openedx/core/djangoapps/user_api/preferences/api.py @@ -12,6 +12,7 @@ from django.db import IntegrityError from django.utils.translation import ugettext as _ from django.utils.translation import ugettext_noop +from openedx.core.lib.time_zone_utils import get_display_time_zone from pytz import common_timezones, common_timezones_set, country_timezones from student.models import User, UserProfile from request_cache import get_request_or_stub @@ -422,8 +423,8 @@ def _create_preference_update_error(preference_key, preference_value, error): def get_country_time_zones(country_code=None): """ - Returns a list of time zones commonly used in given country - or list of all time zones, if country code is None. + Returns a sorted list of time zones commonly used in given + country or list of all time zones, if country code is None. Arguments: country_code (str): ISO 3166-1 Alpha-2 country code @@ -432,7 +433,34 @@ def get_country_time_zones(country_code=None): CountryCodeError: the given country code is invalid """ if country_code is None: - return common_timezones + return _get_sorted_time_zone_list(common_timezones) if country_code.upper() in set(countries.alt_codes): - return country_timezones(country_code) + return _get_sorted_time_zone_list(country_timezones(country_code)) raise CountryCodeError + + +def _get_sorted_time_zone_list(time_zone_list): + """ + Returns a list of time zone dictionaries sorted by their display values + + :param time_zone_list (list): pytz time zone list + """ + return sorted( + [_get_time_zone_dictionary(time_zone) for time_zone in time_zone_list], + key=lambda tz_dict: tz_dict['description'] + ) + + +def _get_time_zone_dictionary(time_zone_name): + """ + Returns a dictionary of time zone information: + + * time_zone: Name of pytz time zone + * description: Display version of time zone [e.g. US/Pacific (PST, UTC-0800)] + + :param time_zone_name (str): Name of pytz time zone + """ + return { + 'time_zone': time_zone_name, + 'description': get_display_time_zone(time_zone_name), + } diff --git a/openedx/core/djangoapps/user_api/preferences/tests/test_api.py b/openedx/core/djangoapps/user_api/preferences/tests/test_api.py index c294b285a1..d02fd37ef3 100644 --- a/openedx/core/djangoapps/user_api/preferences/tests/test_api.py +++ b/openedx/core/djangoapps/user_api/preferences/tests/test_api.py @@ -15,6 +15,7 @@ from django.test import TestCase from django.test.utils import override_settings from dateutil.parser import parse as parse_datetime +from openedx.core.lib.time_zone_utils import get_display_time_zone from student.tests.factories import UserFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -445,13 +446,20 @@ class CountryTimeZoneTest(TestCase): Test cases to validate country code api functionality """ - @ddt.data(('NZ', ['Pacific/Auckland', 'Pacific/Chatham']), - (None, common_timezones)) + @ddt.data(('ES', ['Africa/Ceuta', 'Atlantic/Canary', 'Europe/Madrid']), + (None, common_timezones[:10])) @ddt.unpack def test_get_country_time_zones(self, country_code, expected_time_zones): - """Verify that list of common country time zones are returned""" - country_time_zones = get_country_time_zones(country_code) - self.assertEqual(country_time_zones, expected_time_zones) + """Verify that list of common country time zones dictionaries is returned""" + expected_dict = [ + { + 'time_zone': time_zone, + 'description': get_display_time_zone(time_zone) + } + for time_zone in expected_time_zones + ] + country_time_zones_dicts = get_country_time_zones(country_code)[:10] + self.assertEqual(country_time_zones_dicts, expected_dict) def test_country_code_errors(self): """Verify that country code error is raised for invalid country code""" diff --git a/openedx/core/djangoapps/user_api/serializers.py b/openedx/core/djangoapps/user_api/serializers.py index 3453c2dcb5..19a320bc0c 100644 --- a/openedx/core/djangoapps/user_api/serializers.py +++ b/openedx/core/djangoapps/user_api/serializers.py @@ -4,7 +4,6 @@ Django REST Framework serializers for the User API application from django.contrib.auth.models import User from rest_framework import serializers -from openedx.core.lib.time_zone_utils import get_display_time_zone from student.models import UserProfile from .models import UserPreference @@ -89,17 +88,5 @@ class CountryTimeZoneSerializer(serializers.Serializer): # pylint: disable=abst """ Serializer that generates a list of common time zones for a country """ - time_zone = serializers.SerializerMethodField() - description = serializers.SerializerMethodField() - - def get_time_zone(self, time_zone_name): - """ - Returns inputted time zone name - """ - return time_zone_name - - def get_description(self, time_zone_name): - """ - Returns the display version of time zone [e.g. US/Pacific (PST, UTC-0800)] - """ - return get_display_time_zone(time_zone_name) + time_zone = serializers.CharField() + description = serializers.CharField() diff --git a/openedx/core/djangoapps/user_api/tests/test_views.py b/openedx/core/djangoapps/user_api/tests/test_views.py index 84447555d7..d6668e498e 100644 --- a/openedx/core/djangoapps/user_api/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/tests/test_views.py @@ -1877,6 +1877,7 @@ class TestGoogleRegistrationView( @ddt.ddt +@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class UpdateEmailOptInTestCase(UserAPITestCase, SharedModuleStoreTestCase): """Tests the UpdateEmailOptInPreference view. """ diff --git a/pavelib/i18n.py b/pavelib/i18n.py index 326f31ac70..5767264fe8 100644 --- a/pavelib/i18n.py +++ b/pavelib/i18n.py @@ -75,19 +75,24 @@ def i18n_generate_strict(): @task @needs("pavelib.i18n.i18n_extract") +@cmdopts([ + ("settings=", "s", "The settings to use (defaults to devstack)"), +]) @timed -def i18n_dummy(): +def i18n_dummy(options): """ Simulate international translation by generating dummy strings corresponding to source strings. """ + settings = options.get('settings', DEFAULT_SETTINGS) + sh("i18n_tool dummy") # Need to then compile the new dummy strings sh("i18n_tool generate") # Generate static i18n JS files. for system in ['lms', 'cms']: - sh(django_cmd(system, DEFAULT_SETTINGS, 'compilejsi18n')) + sh(django_cmd(system, settings, 'compilejsi18n')) @task diff --git a/pavelib/paver_tests/test_i18n.py b/pavelib/paver_tests/test_i18n.py index bce412ce69..2dcbda09ac 100644 --- a/pavelib/paver_tests/test_i18n.py +++ b/pavelib/paver_tests/test_i18n.py @@ -2,15 +2,16 @@ Tests for pavelib/i18n.py. """ +import os import textwrap import unittest from mock import mock_open, patch -from paver.easy import task +from paver.easy import task, call_task import pavelib.i18n -from pavelib.paver_tests.utils import PaverTestCase +from pavelib.paver_tests.utils import PaverTestCase TX_CONFIG_SIMPLE = """\ [main] @@ -132,3 +133,36 @@ class ReleasePushPullTest(PaverTestCase): mock_sh.assert_called_once_with( 'i18n_tool transifex pull edx-platform.release-zebrawood edx-platform.release-zebrawood-js' ) + + +class TestI18nDummy(PaverTestCase): + """ + Test the Paver i18n_dummy task. + """ + def setUp(self): + super(TestI18nDummy, self).setUp() + + # Mock the paver @needs decorator for i18n_extract + self._mock_paver_needs = patch.object(pavelib.i18n.i18n_extract, 'needs').start() + self._mock_paver_needs.return_value = 0 + + # Cleanup mocks + self.addCleanup(self._mock_paver_needs.stop) + + def test_i18n_dummy(self): + """ + Test the "i18n_dummy" task. + """ + self.reset_task_messages() + os.environ['NO_PREREQ_INSTALL'] = "true" + call_task('pavelib.i18n.i18n_dummy', options={"settings": 'test'}) + self.assertEquals( + self.task_messages, + [ + u'i18n_tool extract', + u'i18n_tool dummy', + u'i18n_tool generate', + u'python manage.py lms --settings=test compilejsi18n', + u'python manage.py cms --settings=test compilejsi18n', + ] + ) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index cb79d955e5..0d56852d8a 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -183,3 +183,6 @@ pynliner==0.5.2 # for sailthru integration sailthru-client==2.2.3 + +# Release utils for the edx release pipeline +edx-django-release-util==0.1.0 diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index 4d76da9938..1b16d61695 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -55,7 +55,7 @@ git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6 -e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip -e git+https://github.com/jazkarta/edx-jsme.git@c5bfa5d361d6685d8c643838fc0055c25f8b7999#egg=edx-jsme git+https://github.com/edx/django-pyfs.git@1.0.3#egg=django-pyfs==1.0.3 -git+https://github.com/mitocw/django-cas.git@60a5b8e5a62e63e0d5d224a87f0b489201a0c695#egg=django-cas +git+https://github.com/mitodl/django-cas.git@v2.1.1#egg=django-cas -e git+https://github.com/dgrtwo/ParsePy.git@7949b9f754d1445eff8e8f20d0e967b9a6420639#egg=parse_rest # Master pyfs has a bug working with VPC auth. This is a fix. We should switch # back to master when and if this fix is merged back. @@ -77,7 +77,7 @@ git+https://github.com/edx/XBlock.git@xblock-0.4.12#egg=XBlock==0.4.12 -e git+https://github.com/edx/event-tracking.git@0.2.1#egg=event-tracking==0.2.1 -e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2 -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock -git+https://github.com/edx/edx-ora2.git@1.1.6#egg=ora2==1.1.6 +git+https://github.com/edx/edx-ora2.git@1.1.7#egg=ora2==1.1.7 -e git+https://github.com/edx/edx-submissions.git@1.1.1#egg=edx-submissions==1.1.1 git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3 git+https://github.com/edx/i18n-tools.git@v0.3.2#egg=i18n-tools==v0.3.2 @@ -85,7 +85,7 @@ git+https://github.com/edx/edx-val.git@0.0.9#egg=edxval==0.0.9 git+https://github.com/pmitros/RecommenderXBlock.git@v1.1#egg=recommender-xblock==1.1 git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1 -e git+https://github.com/pmitros/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock --e git+https://github.com/pmitros/DoneXBlock.git@857bf365f19c904d7e48364428f6b93ff153fabd#egg=done-xblock +-e git+https://github.com/pmitros/DoneXBlock.git@release-2016-08-10#egg=done-xblock git+https://github.com/edx/edx-milestones.git@v0.1.10#egg=edx-milestones==0.1.10 git+https://github.com/edx/xblock-utils.git@v1.0.2#egg=xblock-utils==1.0.2 -e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive @@ -97,4 +97,4 @@ git+https://github.com/edx/edx-proctoring.git@0.12.21#egg=edx-proctoring==0.12.2 # Third Party XBlocks -e git+https://github.com/mitodl/edx-sga@172a90fd2738f8142c10478356b2d9ed3e55334a#egg=edx-sga -e git+https://github.com/open-craft/xblock-poll@v1.1#egg=xblock-poll==1.1 -git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.0.7#egg=xblock-drag-and-drop-v2==2.0.7 +git+https://github.com/edx-solutions/xblock-drag-and-drop-v2@v2.0.8#egg=xblock-drag-and-drop-v2==2.0.8 diff --git a/scripts/all-tests.sh b/scripts/all-tests.sh index 01b794a6bf..4c120a7c5f 100755 --- a/scripts/all-tests.sh +++ b/scripts/all-tests.sh @@ -12,7 +12,7 @@ set -e # Violations thresholds for failing the build export PYLINT_THRESHOLD=3750 -export ESLINT_THRESHOLD=48129 +export ESLINT_THRESHOLD=10162 SAFELINT_THRESHOLDS=`cat scripts/safelint_thresholds.json` export SAFELINT_THRESHOLDS=${SAFELINT_THRESHOLDS//[[:space:]]/} diff --git a/scripts/create-dev-env.sh b/scripts/create-dev-env.sh deleted file mode 100755 index 75b32feacc..0000000000 --- a/scripts/create-dev-env.sh +++ /dev/null @@ -1,528 +0,0 @@ -#!/usr/bin/env bash - -#Exit if any commands return a non-zero status -set -e - -# posix compliant sanity check -if [ -z $BASH ] || [ $BASH = "/bin/sh" ]; then - echo "Please use the bash interpreter to run this script" - exit 1 -fi - -trap "ouch" ERR - -ouch() { - printf '\E[31m' - - cat</dev/null) 2>/dev/null) || - echo -n "" - - if [[ "x$this_repo" = "xedx-platform.git" ]]; then - # We are in the edx repo and already have git installed. Let git do the - # work of finding base dir: - echo "$(dirname $(git rev-parse --show-toplevel))" - else - echo "$HOME/edx_all" - fi -} - - -### START - -PROG=${0##*/} - -# Adjust this to wherever you'd like to place the codebase -BASE="${PROJECT_HOME:-$(set_base_default)}" - -# Use a sensible default (~/.virtualenvs) for your Python virtualenvs -# unless you've already got one set up with virtualenvwrapper. -PYTHON_DIR=${WORKON_HOME:-"$HOME/.virtualenvs"} - -# Find rbenv root (~/.rbenv by default) -if [ -z "${RBENV_ROOT}" ]; then - RBENV_ROOT="${HOME}/.rbenv" -else - RBENV_ROOT="${RBENV_ROOT%/}" -fi -# Let the repo override the version of Ruby to install -if [[ -r $BASE/edx-platform/.ruby-version ]]; then - RUBY_VER=`cat $BASE/edx-platform/.ruby-version` -fi - -LOG="/var/tmp/install-$(date +%Y%m%d-%H%M%S).log" - -# Make sure the user's not about to do anything dumb -if [[ $EUID -eq 0 ]]; then - error "This script should not be run using sudo or as the root user" - usage - exit 1 -fi - -# If in an existing virtualenv, bail -if [[ "x$VIRTUAL_ENV" != "x" ]]; then - envname=`basename $VIRTUAL_ENV` - error "Looks like you're already in the \"$envname\" virtual env." - error "Run \`deactivate\` and then re-run this script." - usage - exit 1 -fi - -# Read arguments -ARGS=$(getopt "cvhsynq" "$*") -if [[ $? != 0 ]]; then - usage - exit 1 -fi -eval set -- "$ARGS" -while true; do - case $1 in - -c) - compile=true - shift - ;; - -s) - systempkgs=true - shift - ;; - -v) - set -x - verbose=true - shift - ;; - -y) - noninteractive=true - shift - ;; - -q) - quiet=true - shift - ;; - -n) - nopull=true - shift - ;; - -h) - usage - exit 0 - ;; - --) - shift - break - ;; - esac -done - -if [[ ! $quiet ]]; then - cat< >(tee $LOG) -exec 2>&1 - - -# Install basic system requirements - -mkdir -p $BASE -case `uname -s` in - [Ll]inux) - command -v lsb_release &>/dev/null || { - error "Please install lsb-release." - exit 1 - } - - distro=`lsb_release -cs` - case $distro in - wheezy|jessie|maya|olivia|nadia|precise|quantal) - if [[ ! $noninteractive ]]; then - warning " - Debian support is not fully debugged. Assuming you have standard - development packages already working like scipy, the - installation should go fine, but this is still a work in progress. - - Please report issues you have and let us know if you are able to figure - out any workarounds or solutions - - Press return to continue or control-C to abort" - - read dummy - fi - sudo apt-get install -yq git ;; - squeeze|lisa|katya|oneiric|natty|raring) - if [[ ! $noninteractive ]]; then - warning " - It seems like you're using $distro which has been deprecated. - While we don't technically support this release, the install - script will probably still work. - - Press return to continue or control-C to abort" - read dummy - fi - sudo apt-get install -yq git - ;; - - *) - error "Unsupported distribution - $distro" - exit 1 - ;; - esac - ;; - - Darwin) - if [[ ! -w /usr/local ]]; then - cat</dev/null || { - output "Installing brew" - /usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)" - } - command -v git &>/dev/null || { - output "Installing git" - brew install git - } - - ;; - *) - error "Unsupported platform. Try switching to either Mac or a Debian-based linux distribution (Ubuntu, Debian, or Mint)" - exit 1 - ;; -esac - - -# Clone edx repositories - -clone_repos - -# Sanity check to make sure the repo layout hasn't changed -if [[ -d $BASE/edx-platform/scripts ]]; then - output "Installing system-level dependencies" - bash $BASE/edx-platform/scripts/install-system-req.sh -else - error "It appears that our directory structure has changed and somebody failed to update this script. - raise an issue on Github and someone should fix it." - exit 1 -fi - -# Install system-level dependencies -if [[ ! -d $RBENV_ROOT ]]; then - output "Installing rbenv" - git clone https://github.com/sstephenson/rbenv.git $RBENV_ROOT -fi -if [[ ! -d $RBENV_ROOT/plugins/ruby-build ]]; then - output "Installing ruby-build" - git clone https://github.com/sstephenson/ruby-build.git $RBENV_ROOT/plugins/ruby-build -fi -shelltype=$(basename $SHELL) -if ! hash rbenv 2>/dev/null; then - output "Adding rbenv to \$PATH in ~/.${shelltype}rc" - echo "export PATH=\"$RBENV_ROOT/bin:\$PATH\"" >> $HOME/.${shelltype}rc - echo 'eval "$(rbenv init -)"' >> $HOME/.${shelltype}rc - export PATH="$RBENV_ROOT/bin:$PATH" - eval "$(rbenv init -)" -fi - -if [[ ! -d $RBENV_ROOT/versions/$RUBY_VER ]]; then - output "Installing Ruby $RUBY_VER" - rbenv install $RUBY_VER - rbenv global $RUBY_VER -fi - -if ! hash bundle 2>/dev/null; then - output "Installing gem bundler" - gem install bundler -fi -rbenv rehash - -output "Installing ruby packages" -bundle install --gemfile $BASE/edx-platform/Gemfile - -# Install Python virtualenv -output "Installing python virtualenv" - -case `uname -s` in - Darwin) - # Add brew's path - PATH=/usr/local/share/python:/usr/local/bin:$PATH - ;; -esac - -# virtualenvwrapper uses the $WORKON_HOME env var to determine where to place -# virtualenv directories. Make sure it matches the selected $PYTHON_DIR. -export WORKON_HOME=$PYTHON_DIR - -# Load in the mkvirtualenv function if needed -if [[ `type -t mkvirtualenv` != "function" ]]; then - case `uname -s` in - Darwin) - VEWRAPPER=`which virtualenvwrapper.sh` - ;; - - [Ll]inux) - if [[ -f "/etc/bash_completion.d/virtualenvwrapper" ]]; then - VEWRAPPER=/etc/bash_completion.d/virtualenvwrapper - else - error "Could not find virtualenvwrapper" - exit 1 - fi - ;; - esac -fi - -source $VEWRAPPER -# Create edX virtualenv and link it to repo -# virtualenvwrapper automatically sources the activation script -if [[ $systempkgs ]]; then - mkvirtualenv -q -a "$WORKON_HOME" --system-site-packages edx-platform || { - error "mkvirtualenv exited with a non-zero error" - return 1 - } -else - # default behavior for virtualenv>1.7 is - # --no-site-packages - mkvirtualenv -q -a "$WORKON_HOME" edx-platform || { - error "mkvirtualenv exited with a non-zero error" - return 1 - } -fi - - -# compile numpy and scipy if requested - -NUMPY_VER="1.6.2" -SCIPY_VER="0.10.1" - -if [[ -n $compile ]]; then - output "Downloading numpy and scipy" - curl -sSL -o numpy.tar.gz http://downloads.sourceforge.net/project/numpy/NumPy/${NUMPY_VER}/numpy-${NUMPY_VER}.tar.gz - curl -sSL -o scipy.tar.gz http://downloads.sourceforge.net/project/scipy/scipy/${SCIPY_VER}/scipy-${SCIPY_VER}.tar.gz - tar xf numpy.tar.gz - tar xf scipy.tar.gz - rm -f numpy.tar.gz scipy.tar.gz - output "Compiling numpy" - cd "$BASE/numpy-${NUMPY_VER}" - python setup.py install - output "Compiling scipy" - cd "$BASE/scipy-${SCIPY_VER}" - python setup.py install - cd "$BASE" - rm -rf numpy-${NUMPY_VER} scipy-${SCIPY_VER} -fi - -case `uname -s` in - Darwin) - # on mac os x get the latest distribute and pip - pip install -U pip - # need latest pytz before compiling numpy and scipy - pip install -U pytz - pip install numpy - # scipy needs cython - pip install cython - # fixes problem with scipy on 10.8 - pip install -e git+https://github.com/scipy/scipy#egg=scipy-dev - ;; -esac - -output "Installing edX pre-requirements" -pip install -r $BASE/edx-platform/requirements/edx/pre.txt - -output "Installing edX paver-requirements" -pip install -r $BASE/edx-platform/requirements/edx/paver.txt - - -output "Installing edX requirements" -# Install prereqs -cd $BASE/edx-platform -paver install_prereqs - -# Final dependecy -output "Finishing Touches" -cd $BASE -pip install argcomplete -cd $BASE/edx-platform -bundle install -paver install_prereqs - -mkdir -p "$BASE/log" -mkdir -p "$BASE/db" -mkdir -p "$BASE/data" - -./manage.py lms syncdb --noinput --migrate -./manage.py cms syncdb --noinput --migrate - -# Configure Git - -output "Fixing your git default settings" -git config --global push.default current - - -### DONE - -if [[ ! $quiet ]]; then - cat< - - $ ./manage.py lms runserver - - If the Django development server starts properly you - should see: - - Development server is running at http://127.0.0.1:/ - Quit the server with CONTROL-C. - - Connect your browser to http://127.0.0.1: to - view the Django site. - - -END -fi - -exit 0 diff --git a/scripts/install-system-req.sh b/scripts/install-system-req.sh deleted file mode 100755 index b4e5bb04e8..0000000000 --- a/scripts/install-system-req.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env bash - -# posix compliant sanity check -if [ -z $BASH ] || [ $BASH = "/bin/sh" ]; then - echo "Please use the bash interpreter to run this script" - exit 1 -fi - -error() { - printf '\E[31m'; echo "$@"; printf '\E[0m' -} -output() { - printf '\E[36m'; echo "$@"; printf '\E[0m' -} - - -### START - -SELF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -REQUIREMENTS_DIR="$SELF_DIR/../requirements/system" -APT_REPOS_FILE=$REQUIREMENTS_DIR/"ubuntu/apt-repos.txt" -APT_PKGS_FILE=$REQUIREMENTS_DIR/"ubuntu/apt-packages.txt" - -case `uname -s` in - [Ll]inux) - command -v lsb_release &>/dev/null || { - error "Please install lsb-release." - exit 1 - } - - distro=`lsb_release -cs` - case $distro in - #Tries to install the same - squeeze|wheezy|jessie|maya|lisa|olivia|nadia|natty|oneiric|precise|quantal|raring) - output "Installing Debian family requirements" - - # add repositories - cat $APT_REPOS_FILE | xargs -n 1 sudo add-apt-repository -y - sudo apt-get -yq update - sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install gfortran graphviz \ - libgraphviz-dev graphviz-dev libatlas-dev libblas-dev - # install packages listed in APT_PKGS_FILE - cat $APT_PKGS_FILE | xargs sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install - ;; - *) - error "Unsupported distribution - $distro" - exit 1 - ;; - esac - ;; - Darwin) - - if [[ ! -w /usr/local ]]; then - cat</dev/null || { - output "Installing pip" - easy_install pip - } - - if ! grep -Eq ^1.7 <(virtualenv --version 2>/dev/null); then - output "Installing virtualenv >1.7" - pip install 'virtualenv>1.7' virtualenvwrapper - fi - - command -v coffee &>/dev/null || { - output "Installing coffee script" - curl --insecure https://npmjs.org/install.sh | sh - npm install -g coffee-script - } - ;; - *) - error "Unsupported platform" - exit 1 - ;; -esac diff --git a/themes/edx.org/lms/templates/dashboard.html b/themes/edx.org/lms/templates/dashboard.html index 65ac64cb28..917ac69b5c 100644 --- a/themes/edx.org/lms/templates/dashboard.html +++ b/themes/edx.org/lms/templates/dashboard.html @@ -99,8 +99,8 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers <% is_course_blocked = (enrollment.course_id in block_courses) %> <% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %> <% course_requirements = courses_requirements_not_met.get(enrollment.course_id) %> - <% course_program_info = course_programs.get(unicode(enrollment.course_id)) %> - <%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, course_program_info=course_program_info" /> + <% related_programs = programs_by_run.get(unicode(enrollment.course_id)) %> + <%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs" /> % endfor diff --git a/themes/edx.org/lms/templates/footer.html b/themes/edx.org/lms/templates/footer.html index e6b1a09ec4..77949638f1 100644 --- a/themes/edx.org/lms/templates/footer.html +++ b/themes/edx.org/lms/templates/footer.html @@ -18,7 +18,9 @@