diff --git a/cms/djangoapps/contentstore/config/waffle.py b/cms/djangoapps/contentstore/config/waffle.py index f2770c42d1..c473728d78 100644 --- a/cms/djangoapps/contentstore/config/waffle.py +++ b/cms/djangoapps/contentstore/config/waffle.py @@ -2,7 +2,7 @@ This module contains various configuration settings via waffle switches for the contentstore app. """ -from openedx.core.djangoapps.waffle_utils import WaffleFlagNamespace, WaffleSwitchNamespace +from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag, WaffleFlagNamespace, WaffleSwitchNamespace # Namespace WAFFLE_NAMESPACE = u'studio' @@ -23,3 +23,10 @@ def waffle_flags(): Returns the namespaced, cached, audited Waffle Flag class for Studio pages. """ return WaffleFlagNamespace(name=WAFFLE_NAMESPACE, log_prefix=u'Studio: ') + +# Flags +ENABLE_PROCTORING_PROVIDER_OVERRIDES = CourseWaffleFlag( + waffle_namespace=waffle_flags(), + flag_name=u'enable_proctoring_provider_overrides', + flag_undefined_default=False +) diff --git a/cms/djangoapps/contentstore/proctoring.py b/cms/djangoapps/contentstore/proctoring.py index 70d0cbd46e..7659063f7b 100644 --- a/cms/djangoapps/contentstore/proctoring.py +++ b/cms/djangoapps/contentstore/proctoring.py @@ -30,7 +30,6 @@ def register_special_exams(course_key): subsystem. Likewise, if formerly registered exams are unmarked, then those registered exams are marked as inactive """ - if not settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'): # if feature is not enabled then do a quick exit return @@ -72,52 +71,47 @@ def register_special_exams(course_key): ) log.info(msg) + exam_metadata = { + 'exam_name': timed_exam.display_name, + 'time_limit_mins': timed_exam.default_time_limit_minutes, + 'due_date': timed_exam.due, + 'is_proctored': timed_exam.is_proctored_exam, + 'is_practice_exam': timed_exam.is_practice_exam, + 'is_active': True, + 'hide_after_due': timed_exam.hide_after_due, + 'backend': course.proctoring_provider, + } + try: exam = get_exam_by_content_id(unicode(course_key), unicode(timed_exam.location)) # update case, make sure everything is synced - exam_id = update_exam( - exam_id=exam['id'], - exam_name=timed_exam.display_name, - time_limit_mins=timed_exam.default_time_limit_minutes, - due_date=timed_exam.due, - is_proctored=timed_exam.is_proctored_exam, - is_practice_exam=timed_exam.is_practice_exam, - is_active=True, - hide_after_due=timed_exam.hide_after_due, - ) + exam_metadata['exam_id'] = exam['id'] + + exam_id = update_exam(**exam_metadata) msg = 'Updated timed exam {exam_id}'.format(exam_id=exam['id']) log.info(msg) except ProctoredExamNotFoundException: - exam_id = create_exam( - course_id=unicode(course_key), - content_id=unicode(timed_exam.location), - exam_name=timed_exam.display_name, - time_limit_mins=timed_exam.default_time_limit_minutes, - due_date=timed_exam.due, - is_proctored=timed_exam.is_proctored_exam, - is_practice_exam=timed_exam.is_practice_exam, - is_active=True, - hide_after_due=timed_exam.hide_after_due, - ) + exam_metadata['course_id'] = unicode(course_key) + exam_metadata['content_id'] = unicode(timed_exam.location) + + exam_id = create_exam(**exam_metadata) msg = 'Created new timed exam {exam_id}'.format(exam_id=exam_id) log.info(msg) + exam_review_policy_metadata = { + 'exam_id': exam_id, + 'set_by_user_id': timed_exam.edited_by, + 'review_policy': timed_exam.exam_review_rules, + } + # only create/update exam policy for the proctored exams if timed_exam.is_proctored_exam and not timed_exam.is_practice_exam: try: - update_review_policy( - exam_id=exam_id, - set_by_user_id=timed_exam.edited_by, - review_policy=timed_exam.exam_review_rules - ) + update_review_policy(**exam_review_policy_metadata) except ProctoredExamReviewPolicyNotFoundException: if timed_exam.exam_review_rules: # won't save an empty rule. - create_exam_review_policy( - exam_id=exam_id, - set_by_user_id=timed_exam.edited_by, - review_policy=timed_exam.exam_review_rules - ) + create_exam_review_policy(**exam_review_policy_metadata) msg = 'Created new exam review policy with exam_id {exam_id}'.format(exam_id=exam_id) log.info(msg) else: diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py index 18fc69174e..19fd2ce5ae 100644 --- a/cms/djangoapps/contentstore/tests/test_course_settings.py +++ b/cms/djangoapps/contentstore/tests/test_course_settings.py @@ -7,19 +7,24 @@ import json import unittest import ddt -import mock from django.conf import settings +from django.test import RequestFactory from django.test.utils import override_settings from pytz import UTC -from milestones.tests.utils import MilestonesTestCaseMixin +import mock from mock import Mock, patch +from crum import set_current_request +from milestones.tests.utils import MilestonesTestCaseMixin + from contentstore.utils import reverse_course_url, reverse_usage_url +from contentstore.config.waffle import ENABLE_PROCTORING_PROVIDER_OVERRIDES from milestones.models import MilestoneRelationshipType from models.settings.course_grading import CourseGradingModel, GRADING_POLICY_CHANGED_EVENT_TYPE, hash_grading_policy from models.settings.course_metadata import CourseMetadata from models.settings.encoder import CourseSettingsEncoder from openedx.core.djangoapps.models.course_details import CourseDetails +from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag from student.roles import CourseInstructorRole, CourseStaffRole from student.tests.factories import UserFactory from util import milestones_helpers @@ -790,12 +795,18 @@ class CourseMetadataEditingTest(CourseTestCase): shard = 1 def setUp(self): - CourseTestCase.setUp(self) + super(CourseMetadataEditingTest, self).setUp() self.fullcourse = CourseFactory.create() self.course_setting_url = get_url(self.course.id, 'advanced_settings_handler') self.fullcourse_setting_url = get_url(self.fullcourse.id, 'advanced_settings_handler') self.notes_tab = {"type": "notes", "name": "My Notes"} + self.request = RequestFactory().request() + self.user = UserFactory() + self.request.user = self.user + set_current_request(self.request) + self.addCleanup(set_current_request, None) + def test_fetch_initial_fields(self): test_model = CourseMetadata.fetch(self.course) self.assertIn('display_name', test_model, 'Missing editable metadata field') @@ -1189,6 +1200,193 @@ class CourseMetadataEditingTest(CourseTestCase): }) self.assertEqual(response.status_code, 200) + @override_waffle_flag(ENABLE_PROCTORING_PROVIDER_OVERRIDES, True) + def test_proctoring_provider_present_when_waffle_flag_enabled(self): + """ + Tests that proctoring provider field is not filtered out when the waffle flag is enabled. + """ + test_model = CourseMetadata.fetch(self.fullcourse) + self.assertIn('proctoring_provider', test_model) + + @override_settings( + PROCTORING_BACKENDS={ + 'DEFAULT': 'test_proctoring_provider', + 'test_proctoring_provider': {} + } + ) + @override_waffle_flag(ENABLE_PROCTORING_PROVIDER_OVERRIDES, True) + def test_validate_update_does_not_filter_out_proctoring_provider_when_waffle_flag_enabled(self): + """ + Tests that proctoring provider field is returned by validate_and_update_from_json method when + waffle flag is enabled. + """ + field_name = "proctoring_provider" + + _, _, test_model = CourseMetadata.validate_and_update_from_json( + self.course, + { + field_name: {"value": 'test_proctoring_provider'}, + }, + user=self.user + ) + self.assertIn(field_name, test_model) + + @override_settings( + PROCTORING_BACKENDS={ + 'DEFAULT': 'test_proctoring_provider', + 'test_proctoring_provider': {} + } + ) + @override_waffle_flag(ENABLE_PROCTORING_PROVIDER_OVERRIDES, True) + def test_update_from_json_does_not_filter_out_proctoring_provider_when_waffle_flag_enabled(self): + """ + Tests that proctoring provider field is returned by update_from_json method when + waffle flag is enabled. + """ + field_name = "proctoring_provider" + test_model = CourseMetadata.update_from_json( + self.course, + { + field_name: {"value": 'test_proctoring_provider'}, + }, + user=self.user + ) + self.assertIn(field_name, test_model) + + @override_waffle_flag(ENABLE_PROCTORING_PROVIDER_OVERRIDES, False) + def test_proctoring_provider_not_present_when_waffle_flag_not_enabled(self): + """ + Tests that proctoring provider field is filtered out when the waffle flag is not enabled. + """ + test_model = CourseMetadata.fetch(self.fullcourse) + self.assertNotIn('proctoring_provider', test_model) + + @override_waffle_flag(ENABLE_PROCTORING_PROVIDER_OVERRIDES, False) + def test_validate_update_does_filter_out_proctoring_provider_when_waffle_flag_not_enabled(self): + """ + Tests that proctoring provider field is not returned by validate_and_update_from_json method when + waffle flag is not enabled. + """ + field_name = "proctoring_provider" + + _, _, test_model = CourseMetadata.validate_and_update_from_json( + self.course, + { + field_name: {"value": 'test_proctoring_provider'}, + }, + user=self.user + ) + self.assertNotIn(field_name, test_model) + + @override_waffle_flag(ENABLE_PROCTORING_PROVIDER_OVERRIDES, False) + def test_update_from_json_does_filter_out_proctoring_provider_when_waffle_flag_not_enabled(self): + """ + Tests that proctoring provider field is not returned by update_from_json method when + waffle flag is not enabled. + """ + field_name = "proctoring_provider" + + test_model = CourseMetadata.update_from_json( + self.course, + { + field_name: {"value": 'test_proctoring_provider'}, + }, + user=self.user + ) + self.assertNotIn(field_name, test_model) + + def test_create_zendesk_tickets_present_for_edx_staff(self): + """ + Tests that create zendesk tickets field is not filtered out when the user is an edX staff member. + """ + self._set_request_user_to_staff() + + test_model = CourseMetadata.fetch(self.fullcourse) + self.assertIn('create_zendesk_tickets', test_model) + + def test_validate_update_does_not_filter_out_create_zendesk_tickets_for_edx_staff(self): + """ + Tests that create zendesk tickets field is returned by validate_and_update_from_json method when + the user is an edX staff member. + """ + self._set_request_user_to_staff() + + field_name = "create_zendesk_tickets" + + _, _, test_model = CourseMetadata.validate_and_update_from_json( + self.course, + { + field_name: {"value": True}, + }, + user=self.user + ) + self.assertIn(field_name, test_model) + + def test_update_from_json_does_not_filter_out_create_zendesk_tickets_for_edx_staff(self): + """ + Tests that create zendesk tickets field is returned by update_from_json method when + the user is an edX staff member. + """ + self._set_request_user_to_staff() + + field_name = "create_zendesk_tickets" + + test_model = CourseMetadata.update_from_json( + self.course, + { + field_name: {"value": True}, + }, + user=self.user + ) + self.assertIn(field_name, test_model) + + def test_create_zendesk_tickets_not_present_for_course_staff(self): + """ + Tests that create zendesk tickets field is filtered out when the user is not an edX staff member. + """ + test_model = CourseMetadata.fetch(self.fullcourse) + self.assertNotIn('create_zendesk_tickets', test_model) + + def test_validate_update_does_filter_out_create_zendesk_tickets_for_course_staff(self): + """ + Tests that create zendesk tickets field is not returned by validate_and_update_from_json method when + the user is not an edX staff member. + """ + field_name = "create_zendesk_tickets" + + _, _, test_model = CourseMetadata.validate_and_update_from_json( + self.course, + { + field_name: {"value": True}, + }, + user=self.user + ) + self.assertNotIn(field_name, test_model) + + def test_update_from_json_does_filter_out_create_zendesk_tickets_for_course_staff(self): + """ + Tests that create zendesk tickets field is not returned by update_from_json method when + the user is not an edX staff member. + """ + field_name = "create_zendesk_tickets" + + test_model = CourseMetadata.update_from_json( + self.course, + { + field_name: {"value": True}, + }, + user=self.user + ) + self.assertNotIn(field_name, test_model) + + def _set_request_user_to_staff(self): + """ + Update the current request's user to be an edX staff member. + """ + self.user.is_staff = True + self.request.user = self.user + set_current_request(self.request) + class CourseGraderUpdatesTest(CourseTestCase): """ diff --git a/cms/djangoapps/contentstore/tests/test_proctoring.py b/cms/djangoapps/contentstore/tests/test_proctoring.py index b497874459..f273517c45 100644 --- a/cms/djangoapps/contentstore/tests/test_proctoring.py +++ b/cms/djangoapps/contentstore/tests/test_proctoring.py @@ -10,6 +10,7 @@ from mock import patch from pytz import UTC from contentstore.signals.handlers import listen_for_course_publish +from django.conf import settings from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory @@ -31,7 +32,8 @@ class TestProctoredExams(ModuleStoreTestCase): org='edX', course='900', run='test_run', - enable_proctored_exams=True + enable_proctored_exams=True, + proctoring_provider=settings.PROCTORING_BACKENDS['DEFAULT'], ) def _verify_exam_data(self, sequence, expected_active): @@ -61,20 +63,22 @@ class TestProctoredExams(ModuleStoreTestCase): self.assertEqual(exam['is_proctored'], sequence.is_proctored_exam) self.assertEqual(exam['is_practice_exam'], sequence.is_practice_exam) self.assertEqual(exam['is_active'], expected_active) + self.assertEqual(exam['backend'], self.course.proctoring_provider) @ddt.data( - (True, 10, True, False, True, False, False), - (True, 10, False, False, True, False, False), - (True, 10, False, False, True, False, True), - (True, 10, True, True, True, True, False), + (True, False, True, False, False), + (False, False, True, False, False), + (False, False, True, False, True), + (True, True, True, True, False), ) @ddt.unpack - def test_publishing_exam(self, is_time_limited, default_time_limit_minutes, is_proctored_exam, + def test_publishing_exam(self, is_proctored_exam, is_practice_exam, expected_active, republish, hide_after_due): """ Happy path testing to see that when a course is published which contains a proctored exam, it will also put an entry into the exam tables """ + default_time_limit_minutes = 10 chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') sequence = ItemFactory.create( @@ -82,7 +86,7 @@ class TestProctoredExams(ModuleStoreTestCase): category='sequential', display_name='Test Proctored Exam', graded=True, - is_time_limited=is_time_limited, + is_time_limited=True, default_time_limit_minutes=default_time_limit_minutes, is_proctored_exam=is_proctored_exam, is_practice_exam=is_practice_exam, @@ -104,14 +108,13 @@ class TestProctoredExams(ModuleStoreTestCase): listen_for_course_publish(self, self.course.id) # reverify - self._verify_exam_data(sequence, expected_active) + self._verify_exam_data(sequence, expected_active,) def test_unpublishing_proctored_exam(self): """ Make sure that if we publish and then unpublish a proctored exam, the exam record stays, but is marked as is_active=False """ - chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') sequence = ItemFactory.create( parent=chapter, @@ -178,7 +181,6 @@ class TestProctoredExams(ModuleStoreTestCase): """ Make sure the feature flag is honored """ - chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') ItemFactory.create( parent=chapter, diff --git a/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py b/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py index a20122aade..0f66cbc37b 100644 --- a/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py +++ b/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py @@ -2,7 +2,6 @@ Test module for Entrance Exams AJAX callback handler workflows """ import json - from django.conf import settings from django.contrib.auth.models import User from django.test.client import RequestFactory @@ -229,13 +228,14 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): {'entrance_exam_minimum_score_pct': '50'}, http_accept='application/json' ) + self.assertEqual(resp.status_code, 201) resp = self.client.get(self.exam_url) self.assertEqual(resp.status_code, 200) self.course = modulestore().get_course(self.course.id) - # Should raise an ItemNotFoundError and return a 404 updated_metadata = {'entrance_exam_id': 'i4x://org.4/course_4/chapter/ed7c4c6a4d68409998e2c8554c4629d1'} + CourseMetadata.update_from_dict( updated_metadata, self.course, @@ -247,6 +247,7 @@ class EntranceExamHandlerTests(CourseTestCase, MilestonesTestCaseMixin): # Should raise an InvalidKeyError and return a 404 updated_metadata = {'entrance_exam_id': '123afsdfsad90f87'} + CourseMetadata.update_from_dict( updated_metadata, self.course, diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index d9981e5740..20fcf29bf3 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -5,11 +5,14 @@ from django.conf import settings from django.utils.translation import ugettext as _ from six import text_type from xblock.fields import Scope +from crum import get_current_user from xblock_django.models import XBlockStudioConfigurationFlag from xmodule.modulestore.django import modulestore +from student.roles import GlobalStaff from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG +from cms.djangoapps.contentstore.config.waffle import ENABLE_PROCTORING_PROVIDER_OVERRIDES class CourseMetadata(object): @@ -20,9 +23,9 @@ class CourseMetadata(object): editable metadata. ''' # The list of fields that wouldn't be shown in Advanced Settings. - # Should not be used directly. Instead the filtered_list method should + # Should not be used directly. Instead the get_blacklist_of_fields method should # be used if the field needs to be filtered depending on the feature flag. - FILTERED_LIST = [ + FIELDS_BLACK_LIST = [ 'cohort_config', 'xml_attributes', 'start', @@ -65,65 +68,77 @@ class CourseMetadata(object): ] @classmethod - def filtered_list(cls, course_key=None): + def get_blacklist_of_fields(cls, course_key): """ - Filter fields based on feature flag, i.e. enabled, disabled. + Returns a list of fields to not include in Studio Advanced settings based on a + feature flag (i.e. enabled or disabled). """ # Copy the filtered list to avoid permanently changing the class attribute. - filtered_list = list(cls.FILTERED_LIST) + black_list = list(cls.FIELDS_BLACK_LIST) # Do not show giturl if feature is not enabled. if not settings.FEATURES.get('ENABLE_EXPORT_GIT'): - filtered_list.append('giturl') + black_list.append('giturl') # Do not show edxnotes if the feature is disabled. if not settings.FEATURES.get('ENABLE_EDXNOTES'): - filtered_list.append('edxnotes') + black_list.append('edxnotes') # Do not show video auto advance if the feature is disabled if not settings.FEATURES.get('ENABLE_OTHER_COURSE_SETTINGS'): - filtered_list.append('other_course_settings') + black_list.append('other_course_settings') # Do not show video_upload_pipeline if the feature is disabled. if not settings.FEATURES.get('ENABLE_VIDEO_UPLOAD_PIPELINE'): - filtered_list.append('video_upload_pipeline') + black_list.append('video_upload_pipeline') # Do not show video auto advance if the feature is disabled if not settings.FEATURES.get('ENABLE_AUTOADVANCE_VIDEOS'): - filtered_list.append('video_auto_advance') + black_list.append('video_auto_advance') # Do not show social sharing url field if the feature is disabled. if (not hasattr(settings, 'SOCIAL_SHARING_SETTINGS') or not getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}).get("CUSTOM_COURSE_URLS")): - filtered_list.append('social_sharing_url') + black_list.append('social_sharing_url') # Do not show teams configuration if feature is disabled. if not settings.FEATURES.get('ENABLE_TEAMS'): - filtered_list.append('teams_configuration') + black_list.append('teams_configuration') if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'): - filtered_list.append('video_bumper') + black_list.append('video_bumper') # Do not show enable_ccx if feature is not enabled. if not settings.FEATURES.get('CUSTOM_COURSES_EDX'): - filtered_list.append('enable_ccx') - filtered_list.append('ccx_connector') + black_list.append('enable_ccx') + black_list.append('ccx_connector') # Do not show "Issue Open Badges" in Studio Advanced Settings # if the feature is disabled. if not settings.FEATURES.get('ENABLE_OPENBADGES'): - filtered_list.append('issue_badges') + black_list.append('issue_badges') # If the XBlockStudioConfiguration table is not being used, there is no need to # display the "Allow Unsupported XBlocks" setting. if not XBlockStudioConfigurationFlag.is_enabled(): - filtered_list.append('allow_unsupported_xblocks') + black_list.append('allow_unsupported_xblocks') + + # If the ENABLE_PROCTORING_PROVIDER_OVERRIDES waffle flag is not enabled, + # do not show "Proctoring Configuration" in Studio Advanced Settings. + if not ENABLE_PROCTORING_PROVIDER_OVERRIDES.is_enabled(course_key): + black_list.append('proctoring_provider') # Do not show "Course Visibility For Unenrolled Learners" in Studio Advanced Settings # if the enable_anonymous_access flag is not enabled if not COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled(course_key=course_key): - filtered_list.append('course_visibility') - return filtered_list + black_list.append('course_visibility') + + # Do not show "Create Zendesk Tickets For Suspicious Proctored Exam Attempts" in + # Studio Advanced Settings if the user is not edX staff. + if not GlobalStaff().has_user(get_current_user()): + black_list.append('create_zendesk_tickets') + + return black_list @classmethod def fetch(cls, descriptor): @@ -133,8 +148,10 @@ class CourseMetadata(object): """ result = {} metadata = cls.fetch_all(descriptor) + black_list_of_fields = cls.get_blacklist_of_fields(descriptor.id) + for key, value in metadata.iteritems(): - if key in cls.filtered_list(descriptor.id): + if key in black_list_of_fields: continue result[key] = value return result @@ -169,17 +186,17 @@ class CourseMetadata(object): Ensures none of the fields are in the blacklist. """ - filtered_list = cls.filtered_list(descriptor.id) + blacklist_of_fields = cls.get_blacklist_of_fields(descriptor.id) # Don't filter on the tab attribute if filter_tabs is False. if not filter_tabs: - filtered_list.remove("tabs") + blacklist_of_fields.remove("tabs") # Validate the values before actually setting them. key_values = {} for key, model in jsondict.iteritems(): # should it be an error if one of the filtered list items is in the payload? - if key in filtered_list: + if key in blacklist_of_fields: continue try: val = model['value'] @@ -205,11 +222,12 @@ class CourseMetadata(object): errors: list of error objects result: the updated course metadata or None if error """ - filtered_list = cls.filtered_list(descriptor.id) - if not filter_tabs: - filtered_list.remove("tabs") + blacklist_of_fields = cls.get_blacklist_of_fields(descriptor.id) - filtered_dict = dict((k, v) for k, v in jsondict.iteritems() if k not in filtered_list) + if not filter_tabs: + blacklist_of_fields.remove("tabs") + + filtered_dict = dict((k, v) for k, v in jsondict.iteritems() if k not in blacklist_of_fields) did_validate = True errors = [] key_values = {} @@ -238,7 +256,7 @@ class CourseMetadata(object): for key, value in key_values.iteritems(): setattr(descriptor, key, value) - if save and len(key_values): + if save and key_values: modulestore().update_item(descriptor, user.id) return cls.fetch(descriptor) diff --git a/cms/envs/aws.py b/cms/envs/aws.py index 14dae321bb..7fad015159 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -490,11 +490,6 @@ XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False) XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) -################# PROCTORING CONFIGURATION ################## - -PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER) -PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS) - ################# MICROSITE #################### # microsite specific configurations. MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {}) diff --git a/cms/envs/common.py b/cms/envs/common.py index bf1baa22a2..8c79bebfe8 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -881,6 +881,10 @@ WEBPACK_LOADER = { 'DEFAULT': { 'BUNDLE_DIR_NAME': 'bundles/', 'STATS_FILE': os.path.join(STATIC_ROOT, 'webpack-stats.json') + }, + 'WORKERS': { + 'BUNDLE_DIR_NAME': 'bundles/', + 'STATS_FILE': os.path.join(STATIC_ROOT, 'webpack-worker-stats.json') } } WEBPACK_CONFIG_PATH = 'webpack.prod.config.js' @@ -1093,9 +1097,6 @@ INSTALLED_APPS = [ 'xblock_django', - # edX Proctoring - 'edx_proctoring', - # Catalog integration 'openedx.core.djangoapps.catalog', @@ -1445,12 +1446,6 @@ MICROSITE_TEMPLATE_BACKEND = 'microsite_configuration.backends.filebased.Filebas # TTL for microsite database template cache MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = 5 * 60 -############################### PROCTORING CONFIGURATION DEFAULTS ############## -PROCTORING_BACKEND_PROVIDER = { - 'class': 'edx_proctoring.backends.null.NullBackendProvider', - 'options': {}, -} -PROCTORING_SETTINGS = {} ############################ Global Database Configuration ##################### diff --git a/cms/envs/production.py b/cms/envs/production.py index 2fc776a3b7..c528fc3cf9 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -128,6 +128,7 @@ STATIC_ROOT_BASE = ENV_TOKENS.get('STATIC_ROOT_BASE', None) if STATIC_ROOT_BASE: STATIC_ROOT = path(STATIC_ROOT_BASE) / 'studio' WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json" + WEBPACK_LOADER['WORKERS']['STATS_FILE'] = STATIC_ROOT / "webpack-worker-stats.json" EMAIL_BACKEND = ENV_TOKENS.get('EMAIL_BACKEND', EMAIL_BACKEND) EMAIL_FILE_PATH = ENV_TOKENS.get('EMAIL_FILE_PATH', None) @@ -493,11 +494,6 @@ XBLOCK_SETTINGS = ENV_TOKENS.get('XBLOCK_SETTINGS', {}) XBLOCK_SETTINGS.setdefault("VideoDescriptor", {})["licensing_enabled"] = FEATURES.get("LICENSING", False) XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.get('YOUTUBE_API_KEY', YOUTUBE_API_KEY) -################# PROCTORING CONFIGURATION ################## - -PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER) -PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS) - ################# MICROSITE #################### # microsite specific configurations. MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {}) diff --git a/cms/envs/test_static_optimized.py b/cms/envs/test_static_optimized.py index 2874ee494d..64a94efa31 100644 --- a/cms/envs/test_static_optimized.py +++ b/cms/envs/test_static_optimized.py @@ -23,6 +23,7 @@ DATABASES = { } + ######################### PIPELINE #################################### # Use RequireJS optimized storage diff --git a/common/djangoapps/student/roles.py b/common/djangoapps/student/roles.py index 19f4425cc0..da31eb57be 100644 --- a/common/djangoapps/student/roles.py +++ b/common/djangoapps/student/roles.py @@ -120,7 +120,7 @@ class GlobalStaff(AccessRole): The global staff role """ def has_user(self, user): - return user.is_staff + return bool(user and user.is_staff) def add_users(self, *users): for user in users: diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py index 1471489977..c6d39f15a0 100644 --- a/common/lib/xmodule/xmodule/course_module.py +++ b/common/lib/xmodule/xmodule/course_module.py @@ -183,6 +183,89 @@ class TextbookList(List): return json_data +class ProctoringProvider(String): + """ + ProctoringProvider field, which includes validation of the provider + and default that pulls from edx platform settings. + """ + def from_json(self, value): + """ + Return ProctoringProvider as full featured Python type. Perform validation on the provider + and include any inherited values from the platform default. + """ + errors = [] + value = super(ProctoringProvider, self).from_json(value) + + provider_errors = self._validate_proctoring_provider(value) + errors.extend(provider_errors) + + if errors: + raise ValueError(errors) + + value = self._get_proctoring_value(value) + + return value + + def _get_proctoring_value(self, value): + """ + Return a proctoring value that includes any inherited attributes from the platform defaults + for the provider. + """ + # if provider is missing from the value, return the default + if value is None: + return self.default + + return value + + def _validate_proctoring_provider(self, value): + """ + Validate the value for the proctoring provider. If the proctoring provider value is + specified, and it is not one of the providers configured at the platform level, return + a list of error messages to the caller. + """ + errors = [] + + available_providers = get_available_providers() + + if value and value not in available_providers: + errors.append( + _('The selected proctoring provider, {proctoring_provider}, is not a valid provider. ' + 'Please select from one of {available_providers}.') + .format( + proctoring_provider=value, + available_providers=available_providers + ) + ) + + return errors + + @property + def default(self): + """ + Return default value for ProctoringProvider. + """ + default = super(ProctoringProvider, self).default + + proctoring_backend_settings = getattr(settings, 'PROCTORING_BACKENDS', None) + + if proctoring_backend_settings: + return proctoring_backend_settings.get('DEFAULT', None) + + return default + + +def get_available_providers(): + proctoring_backend_settings = getattr( + settings, + 'PROCTORING_BACKENDS', + {} + ) + + available_providers = [provider for provider in proctoring_backend_settings if provider != 'DEFAULT'] + available_providers.sort() + return available_providers + + class CourseFields(object): lti_passports = List( display_name=_("LTI Passports"), @@ -738,6 +821,21 @@ class CourseFields(object): scope=Scope.settings ) + proctoring_provider = ProctoringProvider( + display_name=_("Proctoring Provider"), + help=_( + "Enter the proctoring provider you want to use for this course run. " + "Choose from the following options: {available_providers}."), + help_format_args=dict( + # Put the available providers into a format variable so that translators + # don't translate them. + available_providers=( + ', '.join(get_available_providers()) + ), + ), + scope=Scope.settings, + ) + allow_proctoring_opt_out = Boolean( display_name=_("Allow Opting Out of Proctored Exams"), help=_( @@ -1049,7 +1147,7 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): def definition_to_xml(self, resource_fs): xml_object = super(CourseDescriptor, self).definition_to_xml(resource_fs) - if len(self.textbooks) > 0: + if self.textbooks: textbook_xml_object = etree.Element('textbook') for textbook in self.textbooks: textbook_xml_object.set('title', textbook.title) @@ -1103,7 +1201,8 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin): @raw_grader.setter def raw_grader(self, value): - # NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf + # NOTE WELL: this change will not update the processed graders. + # If we need that, this needs to call grader_from_conf. self._grading_policy['RAW_GRADER'] = value self.grading_policy['GRADER'] = value diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py index 16f53729d9..eb00aa57e5 100644 --- a/common/lib/xmodule/xmodule/tests/test_course_module.py +++ b/common/lib/xmodule/xmodule/tests/test_course_module.py @@ -9,6 +9,8 @@ from fs.memoryfs import MemoryFS from mock import Mock, patch from pytz import utc from xblock.runtime import KvsFieldData, DictKeyValueStore +from django.conf import settings +from django.test import override_settings import xmodule.course_module from xmodule.modulestore.xml import ImportSystem, XMLModuleStore @@ -419,3 +421,73 @@ class CourseDescriptorTestCase(unittest.TestCase): """ expected_certificate_available_date = self.course.end + timedelta(days=2) self.assertEqual(expected_certificate_available_date, self.course.certificate_available_date) + + +class ProctoringProviderTestCase(unittest.TestCase): + """ + Tests for ProctoringProvider, including the default value, validation, and inheritance behavior. + """ + shard = 1 + + def setUp(self): + """ + Initialize dummy testing course. + """ + super(ProctoringProviderTestCase, self).setUp() + self.proctoring_provider = xmodule.course_module.ProctoringProvider() + + def test_from_json_with_platform_default(self): + """ + Test that a proctoring provider value equivalent to the platform + default will pass validation. + """ + default_provider = settings.PROCTORING_BACKENDS.get('DEFAULT') + + # we expect the validated value to be equivalent to the value passed in, + # since there are no validation errors or missing data + self.assertEqual(self.proctoring_provider.from_json(default_provider), default_provider) + + def test_from_json_with_invalid_provider(self): + """ + Test that an invalid provider (i.e. not one configured at the platform level) + throws a ValueError with the correct error message. + """ + provider = 'invalid-provider' + proctoring_provider_whitelist = [u'mock', u'mock_proctoring_without_rules'] + + with self.assertRaises(ValueError) as context_manager: + self.proctoring_provider.from_json(provider) + self.assertEqual( + context_manager.exception.args[0], + ['The selected proctoring provider, {}, is not a valid provider. Please select from one of {}.' + .format(provider, proctoring_provider_whitelist)] + ) + + def test_from_json_adds_platform_default_for_missing_provider(self): + """ + Test that a value with no provider will inherit the default provider + from the platform defaults. + """ + default_provider = 'mock' + + self.assertEqual(self.proctoring_provider.from_json(None), default_provider) + + @override_settings( + PROCTORING_BACKENDS={ + 'mock': {}, + 'mock_proctoring_without_rules': {} + } + ) + def test_default_with_no_platform_default(self): + """ + Test that, when the platform defaults are not set, the default is correct. + """ + self. assertEqual(self.proctoring_provider.default, None) + + @override_settings(PROCTORING_BACKENDS=None) + def test_default_with_no_platform_configuration(self): + """ + Test that, when the platform default is not specified, the default is correct. + """ + default = self.proctoring_provider.default + self.assertEqual(default, None) diff --git a/common/static/common/js/karma.common.conf.js b/common/static/common/js/karma.common.conf.js index 5cff42b208..c0cb71dfd2 100644 --- a/common/static/common/js/karma.common.conf.js +++ b/common/static/common/js/karma.common.conf.js @@ -54,7 +54,7 @@ var webpackConfig = require(path.join(appRoot, 'webpack.dev.config.js')); // https://github.com/webpack-contrib/karma-webpack/issues/24#issuecomment-257613167 // // This should be fixed in v3 of karma-webpack -var commonsChunkPluginIndex = webpackConfig.plugins.findIndex(function(plugin) { return plugin.chunkNames; }); +var commonsChunkPluginIndex = webpackConfig[0].plugins.findIndex(function(plugin) { return plugin.chunkNames; }); // Files which are needed by all lms/cms suites. var commonFiles = { @@ -80,9 +80,9 @@ var commonFiles = { ] }; -webpackConfig.plugins.splice(commonsChunkPluginIndex, 1); +webpackConfig[0].plugins.splice(commonsChunkPluginIndex, 1); -delete webpackConfig.entry; +delete webpackConfig[0].entry; /** * Customize the name attribute in xml testcase element @@ -410,7 +410,7 @@ function getBaseConfig(config, useRequireJs) { captureConsole: false }, - webpack: webpackConfig, + webpack: webpackConfig[0], webpackMiddleware: { watchOptions: { diff --git a/common/test/acceptance/pages/studio/settings_advanced.py b/common/test/acceptance/pages/studio/settings_advanced.py index 2fd3cf858d..69372af6de 100644 --- a/common/test/acceptance/pages/studio/settings_advanced.py +++ b/common/test/acceptance/pages/studio/settings_advanced.py @@ -266,7 +266,6 @@ class AdvancedSettingsPage(CoursePage): 'enable_subsection_gating', 'learning_info', 'instructor_info', - 'create_zendesk_tickets', 'ccx_connector', 'enable_ccx', ] diff --git a/lms/djangoapps/instructor/services.py b/lms/djangoapps/instructor/services.py index d644bfb6f9..8947cef23a 100644 --- a/lms/djangoapps/instructor/services.py +++ b/lms/djangoapps/instructor/services.py @@ -88,7 +88,7 @@ class InstructorService(object): """ return auth.user_has_role(user, CourseStaffRole(CourseKey.from_string(course_id))) - def send_support_notification(self, course_id, exam_name, student_username, review_status): + def send_support_notification(self, course_id, exam_name, student_username, review_status, review_url=None): """ Creates a Zendesk ticket for an exam attempt review from the proctoring system. Currently, it sends notifications for 'Suspicious" status, but additional statuses can be supported @@ -103,15 +103,17 @@ class InstructorService(object): if course.create_zendesk_tickets: requester_name = "edx-proctoring" email = "edx-proctoring@edx.org" - subject = _("Proctored Exam Review: {review_status}").format(review_status=review_status) + subject = _(u"Proctored Exam Review: {review_status}").format(review_status=review_status) body = _( - "A proctored exam attempt for {exam_name} in {course_name} by username: {student_username} " - "was reviewed as {review_status} by the proctored exam review provider." + u"A proctored exam attempt for {exam_name} in {course_name} by username: {student_username} " + "was reviewed as {review_status} by the proctored exam review provider.\n" + "Review link: {review_url}" ).format( exam_name=exam_name, course_name=course.display_name, student_username=student_username, - review_status=review_status + review_status=review_status, + review_url=review_url or u'not available', ) tags = ["proctoring"] create_zendesk_ticket(requester_name, email, subject, body, tags) diff --git a/lms/djangoapps/instructor/tests/test_services.py b/lms/djangoapps/instructor/tests/test_services.py index f52d1da434..84856eac9f 100644 --- a/lms/djangoapps/instructor/tests/test_services.py +++ b/lms/djangoapps/instructor/tests/test_services.py @@ -147,21 +147,40 @@ class InstructorServiceTests(SharedModuleStoreTestCase): """ requester_name = "edx-proctoring" email = "edx-proctoring@edx.org" - subject = "Proctored Exam Review: {review_status}".format(review_status="Suspicious") + subject = u"Proctored Exam Review: {review_status}".format(review_status="Suspicious") + body = "A proctored exam attempt for {exam_name} in {course_name} by username: {student_username} was " \ - "reviewed as {review_status} by the proctored exam review provider." - body = body.format( - exam_name="test_exam", course_name=self.course.display_name, student_username="test_student", - review_status="Suspicious" - ) + "reviewed as {review_status} by the proctored exam review provider.\n" \ + "Review link: {url}" + args = { + 'exam_name': 'test_exam', + 'student_username': 'test_student', + 'url': 'not available', + 'course_name': self.course.display_name, + 'review_status': 'Suspicious', + } + expected_body = body.format(**args) tags = ["proctoring"] with mock.patch("lms.djangoapps.instructor.services.create_zendesk_ticket") as mock_create_zendesk_ticket: self.service.send_support_notification( course_id=unicode(self.course.id), - exam_name="test_exam", - student_username="test_student", - review_status="Suspicious" + exam_name=args['exam_name'], + student_username=args["student_username"], + review_status="Suspicious", + review_url=None, ) - mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, body, tags) + mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, expected_body, tags) + # Now check sending a notification with a review link + args['url'] = 'http://review/url' + with mock.patch("lms.djangoapps.instructor.services.create_zendesk_ticket") as mock_create_zendesk_ticket: + self.service.send_support_notification( + course_id=unicode(self.course.id), + exam_name=args['exam_name'], + student_username=args["student_username"], + review_status="Suspicious", + review_url=args['url'], + ) + expected_body = body.format(**args) + mock_create_zendesk_ticket.assert_called_with(requester_name, email, subject, expected_body, tags) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 439597f85e..3ef99d2e03 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -892,11 +892,6 @@ CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_U JWT_AUTH.update(ENV_TOKENS.get('JWT_AUTH', {})) JWT_AUTH.update(AUTH_TOKENS.get('JWT_AUTH', {})) -################# PROCTORING CONFIGURATION ################## - -PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER) -PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS) - ################# MICROSITE #################### MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {}) MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', '')) diff --git a/lms/envs/common.py b/lms/envs/common.py index b6d939af3e..a64cfd2a41 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1345,28 +1345,6 @@ courseware_js = [ 'js/modules/tab.js', ] -proctoring_js = ( - [ - 'proctoring/js/models/proctored_exam_allowance_model.js', - 'proctoring/js/models/proctored_exam_attempt_model.js', - 'proctoring/js/models/proctored_exam_model.js' - ] + - [ - 'proctoring/js/collections/proctored_exam_allowance_collection.js', - 'proctoring/js/collections/proctored_exam_attempt_collection.js', - 'proctoring/js/collections/proctored_exam_collection.js' - ] + - [ - 'proctoring/js/views/Backbone.ModalDialog.js', - 'proctoring/js/views/proctored_exam_add_allowance_view.js', - 'proctoring/js/views/proctored_exam_allowance_view.js', - 'proctoring/js/views/proctored_exam_attempt_view.js', - 'proctoring/js/views/proctored_exam_view.js' - ] + - [ - 'proctoring/js/proctored_app.js' - ] -) # Before a student accesses courseware, we do not # need many of the JS dependencies. This includes @@ -1693,10 +1671,6 @@ PIPELINE_JS = { ), 'output_filename': 'js/lms-application.js', }, - 'proctoring': { - 'source_filenames': proctoring_js, - 'output_filename': 'js/lms-proctoring.js', - }, 'courseware': { 'source_filenames': courseware_js, 'output_filename': 'js/lms-courseware.js', @@ -1841,6 +1815,10 @@ WEBPACK_LOADER = { 'DEFAULT': { 'BUNDLE_DIR_NAME': 'bundles/', 'STATS_FILE': os.path.join(STATIC_ROOT, 'webpack-stats.json') + }, + 'WORKERS': { + 'BUNDLE_DIR_NAME': 'bundles/', + 'STATS_FILE': os.path.join(STATIC_ROOT, 'webpack-worker-stats.json') } } WEBPACK_CONFIG_PATH = 'webpack.prod.config.js' @@ -2881,9 +2859,6 @@ OPTIONAL_APPS = [ # edxval ('edxval', 'openedx.core.djangoapps.content.course_overviews.apps.CourseOverviewsConfig'), - # edX Proctoring - ('edx_proctoring', None), - # Organizations App (http://github.com/edx/edx-organizations) ('organizations', None), @@ -3215,14 +3190,6 @@ MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = 5 * 60 RSS_PROXY_CACHE_TIMEOUT = 3600 # The length of time we cache RSS retrieved from remote URLs in seconds -#### PROCTORING CONFIGURATION DEFAULTS - -PROCTORING_BACKEND_PROVIDER = { - 'class': 'edx_proctoring.backends.null.NullBackendProvider', - 'options': {}, -} -PROCTORING_SETTINGS = {} - #### Custom Courses for EDX (CCX) configuration # This is an arbitrary hard limit. diff --git a/lms/envs/production.py b/lms/envs/production.py index a98925a535..3cd0ec018e 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -112,6 +112,7 @@ STATIC_ROOT_BASE = ENV_TOKENS.get('STATIC_ROOT_BASE', None) if STATIC_ROOT_BASE: STATIC_ROOT = path(STATIC_ROOT_BASE) WEBPACK_LOADER['DEFAULT']['STATS_FILE'] = STATIC_ROOT / "webpack-stats.json" + WEBPACK_LOADER['WORKERS']['STATS_FILE'] = STATIC_ROOT / "webpack-worker-stats.json" # STATIC_URL_BASE specifies the base url to use for static files @@ -888,11 +889,6 @@ CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_U JWT_AUTH.update(ENV_TOKENS.get('JWT_AUTH', {})) JWT_AUTH.update(AUTH_TOKENS.get('JWT_AUTH', {})) -################# PROCTORING CONFIGURATION ################## - -PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER) -PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS) - ################# MICROSITE #################### MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {}) MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', '')) diff --git a/lms/envs/test_static_optimized.py b/lms/envs/test_static_optimized.py index 25a44eb9e4..bf5bf7f6da 100644 --- a/lms/envs/test_static_optimized.py +++ b/lms/envs/test_static_optimized.py @@ -35,6 +35,11 @@ XQUEUE_INTERFACE = { "basic_auth": ('anant', 'agarwal'), } +PROCTORING_BACKENDS = { + 'DEFAULT': 'mock', + 'mock': {}, + 'mock_proctoring_without_rules': {}, +} ######################### PIPELINE #################################### diff --git a/lms/static/js/instructor_dashboard/instructor_dashboard.js b/lms/static/js/instructor_dashboard/instructor_dashboard.js index 58c65d2c5d..c58e7610e9 100644 --- a/lms/static/js/instructor_dashboard/instructor_dashboard.js +++ b/lms/static/js/instructor_dashboard/instructor_dashboard.js @@ -206,6 +206,9 @@ such that the value can be defined later than this assignment (file load order). }, { constructor: edx.instructor_dashboard.proctoring.ProctoredExamAttemptView, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#special_exams') + }, { + constructor: edx.instructor_dashboard.proctoring.ProctoredExamDashboardView, + $element: idashContent.find('.' + CSS_IDASH_SECTION + '#special_exams') } ]); } diff --git a/lms/static/js/instructor_dashboard/proctoring.js b/lms/static/js/instructor_dashboard/proctoring.js index 22a44b5500..9ef274f9de 100644 --- a/lms/static/js/instructor_dashboard/proctoring.js +++ b/lms/static/js/instructor_dashboard/proctoring.js @@ -7,15 +7,9 @@ $(function() { $proctoringAccordionPane.accordion( { heightStyle: 'content', - activate: function(event, ui) { - var active = $proctoringAccordionPane.accordion('option', 'active'); - $.cookie('saved_index', null); - $.cookie('saved_index', active); - }, animate: 400, header: '> .wrap > .hd', icons: icons, - active: isNaN(parseInt($.cookie('saved_index'))) ? 0 : parseInt($.cookie('saved_index')), collapsible: true } ); diff --git a/lms/templates/instructor/instructor_dashboard_2/special_exams.html b/lms/templates/instructor/instructor_dashboard_2/special_exams.html index a878a94207..f823ea9628 100644 --- a/lms/templates/instructor/instructor_dashboard_2/special_exams.html +++ b/lms/templates/instructor/instructor_dashboard_2/special_exams.html @@ -14,5 +14,9 @@ import pytz

${_('Student Special Exam Attempts')}

+
+

${_('Review Dashboard')}

+
+
diff --git a/lms/urls.py b/lms/urls.py index 762cf863c5..ba2be98a95 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -1012,11 +1012,6 @@ if 'debug_toolbar' in settings.INSTALLED_APPS: url(r'^__debug__/', include(debug_toolbar.urls)), ] -# include into our URL patterns the HTTP REST API that comes with edx-proctoring. -urlpatterns += [ - url(r'^api/', include('edx_proctoring.urls')), -] - if settings.FEATURES.get('ENABLE_FINANCIAL_ASSISTANCE_FORM'): urlpatterns += [ url( diff --git a/openedx/tests/settings.py b/openedx/tests/settings.py index 22da32f525..7403435b4f 100644 --- a/openedx/tests/settings.py +++ b/openedx/tests/settings.py @@ -51,6 +51,12 @@ DATABASES = { } } +PROCTORING_BACKENDS = { + 'DEFAULT': 'mock', + 'mock': {}, + 'mock_proctoring_without_rules': {}, +} + FEATURES = {} INSTALLED_APPS = ( diff --git a/package-lock.json b/package-lock.json index 0cab901273..77241e89fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,6 +85,18 @@ } } }, + "@edx/edx-proctoring": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@edx/edx-proctoring/-/edx-proctoring-1.5.0.tgz", + "integrity": "sha512-RiNjAgh8ZMX0D5gfN2R09a0RBs/R/Blfs/DiqhLmvCSvyCoeMDGANrDDQXv1w5blxxSJbz8a2awSZkwpv6gWNQ==" + }, + "@edx/mockprock": { + "version": "git+https://git@github.com/edx/mockprock.git#c9e4814ace9afad7a778e2af372b3125b3e56588", + "dev": true, + "requires": { + "@edx/edx-proctoring": "1.5.0" + } + }, "@edx/paragon": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-2.6.4.tgz", @@ -247,7 +259,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" }, "accepts": { "version": "1.3.3", @@ -509,7 +521,7 @@ "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=" }, "arr-union": { "version": "3.1.0", @@ -1523,7 +1535,7 @@ "babylon": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==" + "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=" }, "backbone": { "version": "1.3.3", @@ -1728,7 +1740,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + "integrity": "sha1-LN4J617jQfSEdGuwMJsyU7GxRC8=" }, "body-parser": { "version": "1.18.2", @@ -1891,7 +1903,7 @@ "browserify-zlib": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "integrity": "sha1-KGlFnZqjviRf6P4sofRuLn9U1z8=", "requires": { "pako": "1.0.6" } @@ -2182,7 +2194,7 @@ "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "integrity": "sha1-h2Dk7MJy9MNjUy+SbYdKriwTl94=", "requires": { "inherits": "2.0.3", "safe-buffer": "5.1.1" @@ -2203,7 +2215,7 @@ "clap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", - "integrity": "sha512-4CoL/A3hf90V3VIEjeuhSvlGFEHKzOz+Wfc2IVZc+FaUgU0ZQafJTP49fvnULipOPcAfqhyI2duwQyns6xqjYA==", + "integrity": "sha1-TzZ0WzIAhJJVf0ZBLWbVDLmbzlE=", "requires": { "chalk": "1.1.3" }, @@ -2401,7 +2413,7 @@ "color-convert": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "integrity": "sha1-wSYRB66y8pTr/+ye2eytUppgl+0=", "requires": { "color-name": "1.1.3" } @@ -2568,7 +2580,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", "dev": true }, "convert-source-map": { @@ -2608,7 +2620,7 @@ "cosmiconfig": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-3.1.0.tgz", - "integrity": "sha512-zedsBhLSbPBms+kE7AH4vHg6JsKDz6epSv2/+5XHs8ILHlgDciSJfSWf8sX9aQ52Jb7KI7VswUTsLpR/G0cr2Q==", + "integrity": "sha1-ZAqUv5hH8yGABAPNJzr2BmXHM5c=", "dev": true, "requires": { "is-directory": "0.3.1", @@ -2620,7 +2632,7 @@ "esprima": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "integrity": "sha1-RJnt3NERDgshi6zy+n9/WfVcqAQ=", "dev": true }, "js-yaml": { @@ -2707,7 +2719,7 @@ "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "integrity": "sha1-OWz58xN/A+S45TLFj2mCVOAPgOw=", "requires": { "browserify-cipher": "1.0.0", "browserify-sign": "4.0.4", @@ -2945,6 +2957,31 @@ "whatwg-url": "6.5.0" } }, + "datatables": { + "version": "1.10.18", + "resolved": "https://registry.npmjs.org/datatables/-/datatables-1.10.18.tgz", + "integrity": "sha512-ntatMgS9NN6UMpwbmO+QkYJuKlVeMA2Mi0Gu/QxyIh+dW7ZjLSDhPT2tWlzjpIWEkDYgieDzS9Nu7bdQCW0sbQ==", + "requires": { + "jquery": "2.2.4" + } + }, + "datatables.net": { + "version": "1.10.19", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.10.19.tgz", + "integrity": "sha512-+ljXcI6Pj3PTGy5pesp3E5Dr3x3AV45EZe0o1r0gKENN2gafBKXodVnk2ypKwl2tTmivjxbkiqoWnipTefyBTA==", + "requires": { + "jquery": "2.2.4" + } + }, + "datatables.net-fixedcolumns": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/datatables.net-fixedcolumns/-/datatables.net-fixedcolumns-3.2.6.tgz", + "integrity": "sha512-PtEs2tllcHRVZj7fwmAQBWGJ5URRQZpDG2pJsh5jusvnRje3w1+KueMZm60iCtfOkIlUn+/j2+MghxLx/8yfKQ==", + "requires": { + "datatables.net": "1.10.19", + "jquery": "2.2.4" + } + }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", @@ -2958,7 +2995,7 @@ "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", "requires": { "ms": "2.0.0" } @@ -3338,6 +3375,12 @@ } } }, + "edx-proctoring-proctortrack": { + "version": "git+https://git@github.com/joshivj/edx-proctoring-proctortrack.git#66650ed6cd39bf489a86723d5ad3593c2ec8992f", + "requires": { + "@edx/edx-proctoring": "1.5.0" + } + }, "edx-ui-toolkit": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/edx-ui-toolkit/-/edx-ui-toolkit-1.5.2.tgz", @@ -3431,7 +3474,7 @@ "emoji-regex": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", - "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==", + "integrity": "sha1-m66pKbFVVlwR6kHGYm6qZc75ksI=", "dev": true }, "emojis-list": { @@ -4017,7 +4060,7 @@ "eslint-config-airbnb-base": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-11.3.2.tgz", - "integrity": "sha512-/fhjt/VqzBA2SRsx7ErDtv6Ayf+XLw9LIOqmpBuHFCVwyJo2EtzGWMB9fYRFBoWWQLxmNmCpenNiH0RxyeS41w==", + "integrity": "sha1-hwOxGr48iKx+wrdFt/31LgCuaAo=", "dev": true, "requires": { "eslint-restricted-globals": "0.1.1" @@ -4315,7 +4358,7 @@ "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "integrity": "sha1-f8vbGY3HGVlDLv4ThCaE4FJaywI=", "requires": { "md5.js": "1.3.4", "safe-buffer": "5.1.1" @@ -4522,7 +4565,7 @@ "async": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "integrity": "sha1-YaKau2/MAm/qd+VtHG7FOnlZUfQ=", "requires": { "lodash": "4.17.5" } @@ -4899,7 +4942,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0=" }, "function.prototype.name": { "version": "1.1.0", @@ -4987,7 +5030,7 @@ "glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -5017,7 +5060,7 @@ "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==" + "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=" }, "globby": { "version": "7.1.1", @@ -5570,7 +5613,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" } } }, @@ -5582,7 +5625,7 @@ "ignore": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", + "integrity": "sha1-YSKJv7PCIOGGpYEYYY1b6MG6sCE=", "dev": true }, "import-local": { @@ -5851,7 +5894,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" }, "is-builtin-module": { "version": "1.0.0", @@ -6048,7 +6091,7 @@ "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "integrity": "sha1-LBY7P6+xtgbZ0Xko8FwqHDjgdnc=", "requires": { "isobject": "3.0.1" } @@ -8231,7 +8274,7 @@ "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", "requires": { "safe-buffer": "5.1.1" } @@ -8376,7 +8419,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", "requires": { "brace-expansion": "1.1.11" } @@ -8562,7 +8605,7 @@ "node-fetch": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "integrity": "sha1-mA9vcthSEaU0fGsrwYxbhMPrR+8=", "requires": { "encoding": "0.1.12", "is-stream": "1.1.0" @@ -8604,7 +8647,7 @@ "node-libs-browser": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "integrity": "sha1-X5QmPUBPbkR2fXJpAf/wVHjWAN8=", "requires": { "assert": "1.4.1", "browserify-zlib": "0.2.0", @@ -8775,7 +8818,7 @@ "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", "requires": { "hosted-git-info": "2.5.0", "is-builtin-module": "1.0.0", @@ -9203,7 +9246,7 @@ "pako": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==" + "integrity": "sha1-AQEhG6pwxLykoPY/Igbpe3368lg=" }, "parse-asn1": { "version": "5.1.0", @@ -9356,7 +9399,7 @@ "pbkdf2": { "version": "3.0.14", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", - "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "integrity": "sha1-o14TxkeZsGzhUyD0WcIw5o5zut4=", "requires": { "create-hash": "1.1.3", "create-hmac": "1.1.6", @@ -9790,7 +9833,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" } } }, @@ -9878,7 +9921,7 @@ "postcss-reporter": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-5.0.0.tgz", - "integrity": "sha512-rBkDbaHAu5uywbCR2XE8a25tats3xSOsGNx6mppK6Q9kSFGKc/FyAzfci+fWM2l+K402p1D0pNcfDGxeje5IKg==", + "integrity": "sha1-oUF3/RNCgp0pFlPyeG79ZxEDMsM=", "dev": true, "requires": { "chalk": "2.3.1", @@ -9901,7 +9944,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true } } @@ -9964,7 +10007,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true } } @@ -10080,7 +10123,7 @@ "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + "integrity": "sha1-I4Hts2ifelPWUxkAYPz4ItLzaP8=" }, "process": { "version": "0.11.10", @@ -10101,7 +10144,7 @@ "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "integrity": "sha1-BktyYCsY+Q8pGSuLG8QY/9Hr078=", "requires": { "asap": "2.0.6" } @@ -10236,7 +10279,7 @@ "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "integrity": "sha1-x6vpzIuHwLqodrGf3oP9RkeX44w=", "requires": { "is-number": "3.0.0", "kind-of": "4.0.0" @@ -10590,7 +10633,7 @@ "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", "requires": { "safe-buffer": "5.1.1" } @@ -10670,7 +10713,7 @@ "redux": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", - "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "integrity": "sha1-BrcxIyFZAdJdBlvjQusCa8HIU3s=", "requires": { "lodash": "4.17.5", "lodash-es": "4.17.6", @@ -10706,7 +10749,7 @@ "regenerator-transform": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", - "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "integrity": "sha1-HkmWg3Ix2ot/PPQRTXG1aRoGgN0=", "requires": { "babel-runtime": "6.26.0", "babel-types": "6.26.0", @@ -11050,7 +11093,7 @@ "rtlcss": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.2.1.tgz", - "integrity": "sha512-JjQ5DlrmwiItAjlmhoxrJq5ihgZcE0wMFxt7S17bIrt4Lw0WwKKFk+viRhvodB/0falyG/5fiO043ZDh6/aqTw==", + "integrity": "sha1-+FN+QVUggWawXhiYAhMZNvzv0p4=", "requires": { "chalk": "2.3.1", "findup": "0.1.5", @@ -11072,7 +11115,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" } } }, @@ -11094,7 +11137,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=" }, "safe-regex": { "version": "1.1.0", @@ -11488,7 +11531,7 @@ "sass-loader": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.6.tgz", - "integrity": "sha512-c3/Zc+iW+qqDip6kXPYLEgsAu2lf4xz0EZDplB7EmSUMda12U1sGJPetH55B/j9eu0bTtKzKlNPWWyYC7wFNyQ==", + "integrity": "sha1-6dXmwfFV+qMqSybXqbcQfCJeQPk=", "requires": { "async": "2.6.0", "clone-deep": "0.3.0", @@ -11500,7 +11543,7 @@ "async": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "integrity": "sha1-YaKau2/MAm/qd+VtHG7FOnlZUfQ=", "requires": { "lodash": "4.17.5" } @@ -11770,7 +11813,7 @@ "slice-ansi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "integrity": "sha1-BE8aSdiEL/MHqta1Be0Xi9lQE00=", "dev": true, "requires": { "is-fullwidth-code-point": "2.0.0" @@ -12046,7 +12089,7 @@ "source-list-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==" + "integrity": "sha1-qqR0A/eyRakvvJfqCPJQ1gh+0IU=" }, "source-map": { "version": "0.5.7", @@ -12116,7 +12159,7 @@ "specificity": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.3.2.tgz", - "integrity": "sha512-Nc/QN/A425Qog7j9aHmwOrlwX2e7pNI47ciwxwy4jOlvbbMHkNNJchit+FX+UjF3IAdiaaV5BKeWuDUnws6G1A==", + "integrity": "sha1-meZRHs7vD42bV5JJN6rCyxPRPEI=", "dev": true }, "split-string": { @@ -12283,7 +12326,7 @@ "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", "requires": { "safe-buffer": "5.1.1" } @@ -12506,7 +12549,7 @@ "style-loader": { "version": "0.18.2", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.18.2.tgz", - "integrity": "sha512-WPpJPZGUxWYHWIUMNNOYqql7zh85zGmr84FdTVWq52WTIkqlW9xSxD3QYWi/T31cqn9UNSsietVEgGn2aaSCzw==", + "integrity": "sha1-zDFFmvvNbYC3Ig7lSykan9Zv9es=", "requires": { "loader-utils": "1.1.0", "schema-utils": "0.3.0" @@ -12617,7 +12660,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", "dev": true, "requires": { "ms": "2.0.0" @@ -12772,13 +12815,13 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "dev": true, "requires": { "is-fullwidth-code-point": "2.0.0", @@ -12823,7 +12866,7 @@ "stylelint-config-recommended-scss": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-2.0.0.tgz", - "integrity": "sha512-DUIW3daRl5EAyU4ZR6xfPa+bqV5wDccS7X1je6Enes9edpbmWUBR/5XLfDPnjMJgqOe2QwqwaE/qnG4lXID9rg==", + "integrity": "sha1-P0SzOK+zv1tr2e663UaO7ydxOSI=", "dev": true, "requires": { "stylelint-config-recommended": "1.0.0" @@ -12832,7 +12875,7 @@ "stylelint-config-standard": { "version": "17.0.0", "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-17.0.0.tgz", - "integrity": "sha512-G8jMZ0KsaVH7leur9XLZVhwOBHZ2vdbuJV8Bgy0ta7/PpBhEHo6fjVDaNchyCGXB5sRcWVq6O9rEU/MvY9cQDQ==", + "integrity": "sha1-QhA6CQBU7io93p7K7VXl1NnQWfw=", "dev": true, "requires": { "stylelint-config-recommended": "1.0.0" @@ -12841,7 +12884,7 @@ "stylelint-formatter-pretty": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stylelint-formatter-pretty/-/stylelint-formatter-pretty-1.0.3.tgz", - "integrity": "sha512-Jg39kL6kkjUrdKIiHwwz/fbElcF5dOS48ZhvGrEJeWijUbmY1yudclfXv9H61eBqKKu0E33nfez2r0G4EvPtFA==", + "integrity": "sha1-prQ8PzoTIGvft3fQ2ozvxsdsNsM=", "dev": true, "requires": { "ansi-escapes": "2.0.0", @@ -12900,7 +12943,7 @@ "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", "dev": true, "requires": { "is-fullwidth-code-point": "2.0.0", @@ -12955,7 +12998,7 @@ "sugarss": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-1.0.1.tgz", - "integrity": "sha512-3qgLZytikQQEVn1/FrhY7B68gPUUGY3R1Q1vTiD5xT+Ti1DP/8iZuwFet9ONs5+bmL8pZoDQ6JrQHVgrNlK6mA==", + "integrity": "sha1-voJtkAPg8kdzX5I2XcP9fxuunkQ=", "dev": true, "requires": { "postcss": "6.0.19" @@ -12975,7 +13018,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=", "dev": true } } @@ -13446,7 +13489,7 @@ "string_decoder": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", "requires": { "safe-buffer": "5.1.1" } @@ -13469,7 +13512,7 @@ "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", "dev": true, "requires": { "os-tmpdir": "1.0.2" @@ -14403,7 +14446,7 @@ "webpack-merge": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.1.1.tgz", - "integrity": "sha512-geQsZ86YkXOVOjvPC5yv3JSNnL6/X3Kzh935AQ/gJNEYXEfJDQFu/sdFuktS9OW2JcH/SJec8TGfRdrpHshH7A==", + "integrity": "sha1-8Rl6Cpc+acb77rbWWCGaqMDBNVU=", "requires": { "lodash": "4.17.5" } @@ -14420,7 +14463,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha1-dHIq8y6WFOnCh6jQu95IteLxomM=" } } }, @@ -14571,7 +14614,7 @@ "xml2js": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "integrity": "sha1-aGwg8hMgnpSr8NG88e+qKRx4J6c=", "dev": true, "requires": { "sax": "1.2.4", diff --git a/package.json b/package.json index 53933f92b5..d9da99f345 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "dependencies": { "@edx/cookie-policy-banner": "1.1.10", "@edx/edx-bootstrap": "1.0.3", + "@edx/edx-proctoring": "^1.5.0", "@edx/paragon": "2.6.4", "@edx/studio-frontend": "1.16.12", "babel-core": "6.26.0", @@ -21,7 +22,10 @@ "camelize": "1.0.0", "classnames": "2.2.5", "css-loader": "0.28.8", + "datatables": "1.10.18", + "datatables.net-fixedcolumns": "3.2.6", "edx-pattern-library": "0.18.1", + "edx-proctoring-proctortrack": "git+https://git@github.com/joshivj/edx-proctoring-proctortrack.git", "edx-ui-toolkit": "1.5.2", "exports-loader": "0.6.4", "extract-text-webpack-plugin": "2.1.2", @@ -63,6 +67,7 @@ "which-country": "1.0.0" }, "devDependencies": { + "@edx/mockprock": "^1.0.1", "@edx/stylelint-config-edx": "1.1.0", "babel-jest": "23.0.1", "edx-custom-a11y-rules": "1.0.5", diff --git a/requirements/constraints.txt b/requirements/constraints.txt index a77a74eb9d..36a494baa8 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -12,8 +12,6 @@ # https://github.com/transifex/transifex-client/issues/252 six==1.11.0 - - # Convert text markup to HTML; used in capa problems, forums, and course wikis; pin Markdown version as tests failed for its upgrade to latest release Markdown==2.6.11 @@ -21,9 +19,6 @@ Markdown==2.6.11 # Can be removed when we get to Python 3. pylint-plugin-utils==0.3 -# Testing framework # Pinned due to https://github.com/pytest-dev/pytest/issues/3749 -pytest==3.6.3 - # pytest plugin for measuring code coverage. # Pinned due to https://openedx.atlassian.net/browse/TE-2731 pytest-cov<2.6 diff --git a/requirements/edx/base.in b/requirements/edx/base.in index e36a17747f..869932ca8a 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -80,7 +80,7 @@ edx-enterprise edx-milestones edx-oauth2-provider edx-organizations -edx-proctoring +edx-proctoring>=1.5.0 edx-rest-api-client edx-search edx-submissions @@ -133,7 +133,7 @@ rfc6266-parser # Used to generate Content-Disposition heade social-auth-app-django<3.0.0 social-auth-core<2.0.0 pysrt==0.4.7 # Support for SubRip subtitle files, used in the video XModule -pytz==2016.10 # Time zone information database +pytz # Time zone information database PyYAML # Used to parse XModule resource templates redis==2.10.6 # celery task broker requests-oauthlib # Simplifies use of OAuth via the requests library, used for CCX and LTI diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 2c4047545d..e670657dfe 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -118,13 +118,13 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.0.1 -edx-enterprise==1.2.0 +edx-enterprise==1.2.1 edx-i18n-tools==0.4.6 edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 edx-opaque-keys[django]==0.4.4 edx-organizations==1.0.0 -edx-proctoring==1.4.0 +edx-proctoring==1.5.0 edx-rest-api-client==1.9.2 edx-search==1.2.1 edx-submissions==2.0.12 @@ -204,7 +204,7 @@ python-memcached==1.48 python-openid==2.2.5 python-saml==2.4.0 python-swiftclient==3.6.0 -pytz==2016.10 +pytz==2018.7 pyuca==1.1 pyyaml==3.13 redis==2.10.6 diff --git a/requirements/edx/coverage.in b/requirements/edx/coverage.in index 26a1299140..a0c036431a 100644 --- a/requirements/edx/coverage.in +++ b/requirements/edx/coverage.in @@ -14,3 +14,4 @@ coverage==4.4 # Code coverage testing for Python diff-cover==0.9.8 # Automatically find diff lines that need test coverage +six==1.11.0 # Pinned because diff-cover needs it, but later transifex-client says ==1.11.0 diff --git a/requirements/edx/coverage.txt b/requirements/edx/coverage.txt index a632cc4a62..0be086ff78 100644 --- a/requirements/edx/coverage.txt +++ b/requirements/edx/coverage.txt @@ -11,4 +11,4 @@ jinja2-pluralize==0.3.0 # via diff-cover jinja2==2.10 # via diff-cover, jinja2-pluralize markupsafe==1.1.0 # via jinja2 pygments==2.3.1 # via diff-cover -six==1.11.0 # via diff-cover +six==1.11.0 diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 3468db47fc..cfaf668b26 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -137,14 +137,14 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.0.1 -edx-enterprise==1.2.0 +edx-enterprise==1.2.1 edx-i18n-tools==0.4.6 edx-lint==1.0.0 edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 edx-opaque-keys[django]==0.4.4 edx-organizations==1.0.0 -edx-proctoring==1.4.0 +edx-proctoring==1.5.0 edx-rest-api-client==1.9.2 edx-search==1.2.1 edx-sphinx-theme==1.4.0 @@ -228,6 +228,7 @@ pa11ycrawler==1.6.2 packaging==18.0 # via sphinx parsel==1.5.1 path.py==8.2.1 +pathlib2==2.3.3 pathtools==0.1.2 paver==1.3.4 pbr==5.1.1 @@ -235,7 +236,7 @@ pdfminer==20140328 piexif==1.0.2 pillow==5.3.0 pip-tools==3.2.0 -pluggy==0.6.0 +pluggy==0.8.0 polib==1.1.0 psutil==1.2.1 py2neo==3.1.2 @@ -271,7 +272,7 @@ pytest-django==3.1.2 pytest-forked==0.2 pytest-randomly==1.2.3 pytest-xdist==1.25.0 -pytest==3.6.3 +pytest==4.0.2 python-dateutil==2.4.0 python-levenshtein==0.12.0 python-memcached==1.48 @@ -281,7 +282,7 @@ python-saml==2.4.0 python-slugify==1.2.6 python-subunit==1.3.0 python-swiftclient==3.6.0 -pytz==2016.10 +pytz==2018.7 pyuca==1.1 pyyaml==3.13 queuelib==1.5.0 @@ -295,6 +296,7 @@ rfc6266-parser==0.0.5.post2 rules==2.0.1 s3transfer==0.1.13 sailthru-client==2.2.3 +scandir==1.9.0 scipy==0.14.0 scrapy==1.1.2 selenium==3.141.0 @@ -312,20 +314,20 @@ social-auth-app-django==2.1.0 social-auth-core==1.7.0 sorl-thumbnail==12.3 sortedcontainers==0.9.2 -sphinx==1.8.2 +sphinx==1.8.3 sphinxcontrib-websupport==1.1.0 # via sphinx splinter==0.9.0 sqlparse==0.2.4 stevedore==1.10.0 sure==1.4.11 sympy==0.7.1 -testfixtures==6.4.0 +testfixtures==6.4.1 testtools==2.3.0 text-unidecode==1.2 tincan==0.0.5 toml==0.10.0 tox-battery==0.5.1 -tox==3.6.0 +tox==3.6.1 traceback2==1.4.0 transifex-client==0.13.5 twisted==16.6.0 diff --git a/requirements/edx/paver.in b/requirements/edx/paver.in index 85b0656b3c..36b1cb4054 100644 --- a/requirements/edx/paver.in +++ b/requirements/edx/paver.in @@ -24,3 +24,5 @@ requests # Simple interface for making HTTP requests stevedore==1.10.0 # via edx-opaque-keys watchdog # Used in paver watch_assets wrapt==1.10.5 # Decorator utilities used in the @timed paver task decorator + +six==1.11.0 # Pinned because a few things here need it, but later transifex-client says ==1.11.0 diff --git a/requirements/edx/paver.txt b/requirements/edx/paver.txt index f0d2c0689c..979d82023a 100644 --- a/requirements/edx/paver.txt +++ b/requirements/edx/paver.txt @@ -23,7 +23,7 @@ pymongo==2.9.1 python-memcached==1.48 pyyaml==3.13 # via watchdog requests==2.21.0 -six==1.11.0 # via edx-opaque-keys, libsass, paver, stevedore +six==1.11.0 stevedore==1.10.0 urllib3==1.23 # via requests watchdog==0.9.0 diff --git a/requirements/edx/pip-tools.in b/requirements/edx/pip-tools.in index 50f3de3e19..098843a697 100644 --- a/requirements/edx/pip-tools.in +++ b/requirements/edx/pip-tools.in @@ -10,3 +10,4 @@ -c ../constraints.txt pip-tools # Contains pip-compile, used to generate pip requirements files +six==1.11.0 # Pinned because pip-tools needs it, but later transifex-client says ==1.11.0 diff --git a/requirements/edx/pip-tools.txt b/requirements/edx/pip-tools.txt index 546f9d2811..feb69c6ac1 100644 --- a/requirements/edx/pip-tools.txt +++ b/requirements/edx/pip-tools.txt @@ -6,4 +6,4 @@ # click==7.0 # via pip-tools pip-tools==3.2.0 -six==1.11.0 # via pip-tools +six==1.11.0 diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index ce1bd72028..fb9c730de4 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -132,14 +132,14 @@ edx-django-release-util==0.3.1 edx-django-sites-extensions==2.3.1 edx-django-utils==1.0.3 edx-drf-extensions==2.0.1 -edx-enterprise==1.2.0 +edx-enterprise==1.2.1 edx-i18n-tools==0.4.6 edx-lint==1.0.0 edx-milestones==0.1.13 edx-oauth2-provider==1.2.2 edx-opaque-keys[django]==0.4.4 edx-organizations==1.0.0 -edx-proctoring==1.4.0 +edx-proctoring==1.5.0 edx-rest-api-client==1.9.2 edx-search==1.2.1 edx-submissions==2.0.12 @@ -219,13 +219,14 @@ openapi-codec==1.3.2 pa11ycrawler==1.6.2 parsel==1.5.1 # via scrapy path.py==8.2.1 +pathlib2==2.3.3 # via pytest pathtools==0.1.2 paver==1.3.4 pbr==5.1.1 pdfminer==20140328 piexif==1.0.2 pillow==5.3.0 -pluggy==0.6.0 # via pytest, tox +pluggy==0.8.0 # via pytest, tox polib==1.1.0 psutil==1.2.1 py2neo==3.1.2 @@ -246,7 +247,7 @@ pyjwt==1.5.2 pylint-celery==0.3 # via edx-lint pylint-django==0.7.2 # via edx-lint pylint-plugin-utils==0.3 # via pylint-celery, pylint-django -pylint==1.7.6 # via edx-lint, pylint-celery, pylint-django, pylint-plugin-utils +pylint==1.7.6 # via edx-lint, pylint-celery, pylint-django pymongo==2.9.1 pynliner==0.8.0 pyopenssl==18.0.0 # via scrapy @@ -260,7 +261,7 @@ pytest-django==3.1.2 pytest-forked==0.2 # via pytest-xdist pytest-randomly==1.2.3 pytest-xdist==1.25.0 -pytest==3.6.3 +pytest==4.0.2 python-dateutil==2.4.0 python-levenshtein==0.12.0 python-memcached==1.48 @@ -270,7 +271,7 @@ python-saml==2.4.0 python-slugify==1.2.6 # via transifex-client python-subunit==1.3.0 python-swiftclient==3.6.0 -pytz==2016.10 +pytz==2018.7 pyuca==1.1 pyyaml==3.13 queuelib==1.5.0 # via scrapy @@ -284,6 +285,7 @@ rfc6266-parser==0.0.5.post2 rules==2.0.1 s3transfer==0.1.13 sailthru-client==2.2.3 +scandir==1.9.0 # via pathlib2 scipy==0.14.0 scrapy==1.1.2 # via pa11ycrawler selenium==3.141.0 @@ -304,13 +306,13 @@ sqlparse==0.2.4 stevedore==1.10.0 sure==1.4.11 sympy==0.7.1 -testfixtures==6.4.0 +testfixtures==6.4.1 testtools==2.3.0 # via fixtures, python-subunit text-unidecode==1.2 # via faker tincan==0.0.5 toml==0.10.0 # via tox tox-battery==0.5.1 -tox==3.6.0 +tox==3.6.1 traceback2==1.4.0 # via testtools, unittest2 transifex-client==0.13.5 twisted==16.6.0 # via pa11ycrawler, scrapy diff --git a/webpack.common.config.js b/webpack.common.config.js index 0cc4624926..93c59deb75 100644 --- a/webpack.common.config.js +++ b/webpack.common.config.js @@ -26,362 +26,405 @@ var defineFooter = new RegExp('(' + defineCallFooter.source + ')|(' + defineDirectFooter.source + ')|(' + defineFancyFooter.source + ')', 'm'); -module.exports = Merge.smart({ - context: __dirname, - - entry: { - // Studio - Import: './cms/static/js/features/import/factories/import.js', - CourseOrLibraryListing: './cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx', - 'js/factories/login': './cms/static/js/factories/login.js', - 'js/factories/textbooks': './cms/static/js/factories/textbooks.js', - 'js/factories/container': './cms/static/js/factories/container.js', - 'js/factories/context_course': './cms/static/js/factories/context_course.js', - 'js/factories/library': './cms/static/js/factories/library.js', - 'js/factories/xblock_validation': './cms/static/js/factories/xblock_validation.js', - 'js/factories/edit_tabs': './cms/static/js/factories/edit_tabs.js', - 'js/sock': './cms/static/js/sock.js', - - // LMS - SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx', - AlertStatusBar: './lms/static/js/accessible_components/StatusBarAlert.jsx', - LearnerAnalyticsDashboard: './lms/static/js/learner_analytics_dashboard/LearnerAnalyticsDashboard.jsx', - UpsellExperimentModal: './lms/static/common/js/components/UpsellExperimentModal.jsx', - PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx', - EntitlementSupportPage: './lms/djangoapps/support/static/support/jsx/entitlements/index.jsx', - PasswordResetConfirmation: './lms/static/js/student_account/components/PasswordResetConfirmation.jsx', - StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx', - StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js', - ProblemBrowser: './lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx', - - // Learner Dashboard - EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js', - EntitlementUnenrollmentFactory: './lms/static/js/learner_dashboard/entitlement_unenrollment_factory.js', - ProgramDetailsFactory: './lms/static/js/learner_dashboard/program_details_factory.js', - ProgramListFactory: './lms/static/js/learner_dashboard/program_list_factory.js', - UnenrollmentFactory: './lms/static/js/learner_dashboard/unenrollment_factory.js', - CompletionOnViewService: './lms/static/completion/js/CompletionOnViewService.js', - - // Features - CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js', - CourseHome: './openedx/features/course_experience/static/course_experience/js/CourseHome.js', - CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js', - CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js', - CourseTalkReviews: './openedx/features/course_experience/static/course_experience/js/CourseTalkReviews.js', - Currency: './openedx/features/course_experience/static/course_experience/js/currency.js', - Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js', - LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.js', - WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js', - - CookiePolicyBanner: './common/static/js/src/CookiePolicyBanner.jsx', - - // Common - ReactRenderer: './common/static/js/src/ReactRenderer.jsx', - XModuleShim: 'xmodule/js/src/xmodule.js', - - VerticalStudentView: './common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js', - commons: 'babel-polyfill' - }, - - output: { - path: path.resolve(__dirname, 'common/static/bundles'), - libraryTarget: 'window' - }, - - plugins: [ - new webpack.NoEmitOnErrorsPlugin(), - new webpack.NamedModulesPlugin(), - new BundleTracker({ - path: process.env.STATIC_ROOT_CMS, - filename: 'webpack-stats.json' - }), - new BundleTracker({ - path: process.env.STATIC_ROOT_LMS, - filename: 'webpack-stats.json' - }), - new webpack.ProvidePlugin({ - _: 'underscore', - $: 'jquery', - jQuery: 'jquery', - 'window.jQuery': 'jquery', - Popper: 'popper.js', // used by bootstrap - CodeMirror: 'codemirror', - 'edx.HtmlUtils': 'edx-ui-toolkit/js/utils/html-utils', - AjaxPrefix: 'ajax_prefix', - // This is used by some XModules/XBlocks, which don't have - // any other way to declare that dependency. - $script: 'scriptjs' - }), - - // Note: Until karma-webpack releases v3, it doesn't play well with - // the CommonsChunkPlugin. We have a kludge in karma.common.conf.js - // that dynamically removes this plugin from webpack config when - // running those tests (the details are in that file). This is a - // recommended workaround, as this plugin is just an optimization. But - // because of this, we really don't want to get too fancy with how we - // invoke this plugin until we can upgrade karma-webpack. - new webpack.optimize.CommonsChunkPlugin({ - // If the value below changes, update the render_bundle call in - // common/djangoapps/pipeline_mako/templates/static_content.html - name: 'commons', - filename: 'commons.js', - minChunks: 3 - }) - ], - - module: { - noParse: [ - // See sinon/webpack interaction weirdness: - // https://github.com/webpack/webpack/issues/304#issuecomment-272150177 - // (I've tried every other suggestion solution on that page, this - // was the only one that worked.) - /\/sinon\.js|codemirror-compressed\.js|hls\.js|tinymce\.full\.min\.js/ - ], - rules: [ - { - test: files.namespacedRequire.concat(files.textBangUnderscore, filesWithRequireJSBlocks), - loader: StringReplace.replace( - ['babel-loader'], - { - replacements: [ - { - pattern: defineHeader, - replacement: function() { return ''; } - }, - { - pattern: defineFooter, - replacement: function() { return ''; } - }, - { - pattern: /(\/\* RequireJS) \*\//g, - replacement: function(match, p1) { return p1; } - }, - { - pattern: /\/\* Webpack/g, - replacement: function(match) { return match + ' */'; } - }, - { - pattern: /text!(.*?\.underscore)/g, - replacement: function(match, p1) { return p1; } - }, - { - pattern: /RequireJS.require/g, - replacement: function() { - return 'require'; - } - } - ] - } - ) - }, - { - test: /\.(js|jsx)$/, - exclude: [ - /node_modules/, - files.namespacedRequire, - files.textBangUnderscore, - filesWithRequireJSBlocks +var workerConfig = function() { + try { + return { + webworker: { + target: 'webworker', + context: __dirname, + entry: require('../workers.json'), + output: { + filename: '[name].js', + path: path.resolve(__dirname, 'common/static/bundles') + }, + plugins: [ + new BundleTracker({ + path: process.env.STATIC_ROOT_LMS, + filename: 'webpack-worker-stats.json' + }) ], - use: 'babel-loader' - }, - { - test: /\.(js|jsx)$/, - include: [ - /paragon/ - ], - use: 'babel-loader' - }, - { - test: path.resolve(__dirname, 'common/static/js/src/ajax_prefix.js'), - use: [ - 'babel-loader', - { - loader: 'exports-loader', - options: { - 'this.AjaxPrefix': true + module: { + rules: [ + { + test: /\.(js|jsx)$/, + include: [ + /node_modules\// + ], + use: 'babel-loader' } - } - ] - }, - { - test: /\.underscore$/, - use: 'raw-loader' - }, - { - // This file is used by both RequireJS and Webpack and depends on window globals - // This is a dirty hack and shouldn't be replicated for other files. - test: path.resolve(__dirname, 'cms/static/cms/js/main.js'), - loader: StringReplace.replace( - ['babel-loader'], - { - replacements: [ - { - pattern: /\(function\(AjaxPrefix\) {/, - replacement: function() { return ''; } - }, - { - pattern: /], function\(domReady, \$, str, Backbone, gettext, NotificationView\) {/, - replacement: function() { - // eslint-disable-next-line - return '], function(domReady, $, str, Backbone, gettext, NotificationView, AjaxPrefix) {'; - } - }, - { - pattern: /'..\/..\/common\/js\/components\/views\/feedback_notification',/, - replacement: function() { - return "'../../common/js/components/views/feedback_notification', 'AjaxPrefix',"; - } - }, - { - pattern: /}\).call\(this, AjaxPrefix\);/, - replacement: function() { return ''; } - }, - { - pattern: /'..\/..\/common\/js\/components\/views\/feedback_notification',/, - replacement: function() { - return "'../../common/js/components/views/feedback_notification', 'AjaxPrefix',"; - } - } - ] - } - ) - }, - { - test: /\.(woff2?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, - loader: 'file-loader' - }, - { - test: /\.svg$/, - loader: 'svg-inline-loader' - }, - { - test: /xblock\/core/, - loader: 'exports-loader?window.XBlock!imports-loader?jquery,jquery.immediateDescendents,this=>window' - }, - { - test: /xblock\/runtime.v1/, - loader: 'exports-loader?window.XBlock!imports-loader?XBlock=xblock/core,this=>window' - }, - { - test: /descriptors\/js/, - loader: 'imports-loader?this=>window' - }, - { - test: /modules\/js/, - loader: 'imports-loader?this=>window' - }, - { - test: /codemirror/, - loader: 'exports-loader?window.CodeMirror' - }, - { - test: /tinymce/, - loader: 'imports-loader?this=>window' - }, - { - test: /xmodule\/js\/src\/xmodule/, - loader: 'exports-loader?window.XModule!imports-loader?this=>window' - }, - { - test: /mock-ajax/, - loader: 'imports-loader?exports=>false' - }, - { - test: /d3.min/, - use: [ - 'babel-loader', - { - loader: 'exports-loader', - options: { - d3: true - } - } - ] - }, - { - test: /logger/, - loader: 'imports-loader?this=>window' + ] + }, + resolve: { + extensions: ['.js'] + } } - ] - }, - - resolve: { - extensions: ['.js', '.jsx', '.json'], - alias: { - AjaxPrefix: 'ajax_prefix', - accessibility: 'accessibility_tools', - codemirror: 'codemirror-compressed', - datepair: 'timepicker/datepair', - 'edx-ui-toolkit': 'edx-ui-toolkit/src/', // @TODO: some paths in toolkit are not valid relative paths - ieshim: 'ie_shim', - jquery: 'jquery/src/jquery', // Use the non-diqst form of jQuery for better debugging + optimization - 'jquery.flot': 'flot/jquery.flot.min', - 'jquery.ui': 'jquery-ui.min', - 'jquery.tinymce': 'jquery.tinymce.min', - 'jquery.inputnumber': 'html5-input-polyfills/number-polyfill', - 'jquery.qtip': 'jquery.qtip.min', - 'jquery.smoothScroll': 'jquery.smooth-scroll.min', - 'jquery.timepicker': 'timepicker/jquery.timepicker', - 'backbone.associations': 'backbone-associations/backbone-associations-min', - squire: 'Squire', - tinymce: 'tinymce.full.min', - - // See sinon/webpack interaction weirdness: - // https://github.com/webpack/webpack/issues/304#issuecomment-272150177 - // (I've tried every other suggestion solution on that page, this - // was the only one that worked.) - sinon: __dirname + '/node_modules/sinon/pkg/sinon.js', - hls: 'hls.js/dist/hls.js' - }, - modules: [ - 'cms/djangoapps/pipeline_js/js', - 'cms/static', - 'cms/static/cms/js', - 'cms/templates/js', - 'lms/static', - 'common/lib/xmodule', - 'common/lib/xmodule/xmodule/js/src', - 'common/lib/xmodule/xmodule/assets/word_cloud/src/js', - 'common/static', - 'common/static/coffee/src', - 'common/static/common/js', - 'common/static/common/js/vendor/', - 'common/static/common/js/components', - 'common/static/js/src', - 'common/static/js/vendor/', - 'common/static/js/vendor/jQuery-File-Upload/js/', - 'common/static/js/vendor/tinymce/js/tinymce', - 'node_modules', - 'common/static/xmodule' - ] - }, - - resolveLoader: { - alias: { - text: 'raw-loader' // Compatibility with RequireJSText's text! loader, uses raw-loader under the hood } - }, - - externals: { - $: 'jQuery', - backbone: 'Backbone', - canvas: 'canvas', - coursetalk: 'CourseTalk', - gettext: 'gettext', - jquery: 'jQuery', - logger: 'Logger', - underscore: '_', - URI: 'URI', - XBlockToXModuleShim: 'XBlockToXModuleShim', - XModule: 'XModule' - }, - - watchOptions: { - poll: true - }, - - node: { - fs: 'empty' + } catch (err) { + return null; } +}; + +module.exports = Merge.smart({ + web: { + context: __dirname, + + entry: { + // Studio + Import: './cms/static/js/features/import/factories/import.js', + CourseOrLibraryListing: './cms/static/js/features_jsx/studio/CourseOrLibraryListing.jsx', + 'js/factories/login': './cms/static/js/factories/login.js', + 'js/factories/textbooks': './cms/static/js/factories/textbooks.js', + 'js/factories/container': './cms/static/js/factories/container.js', + 'js/factories/context_course': './cms/static/js/factories/context_course.js', + 'js/factories/library': './cms/static/js/factories/library.js', + 'js/factories/xblock_validation': './cms/static/js/factories/xblock_validation.js', + 'js/factories/edit_tabs': './cms/static/js/factories/edit_tabs.js', + 'js/sock': './cms/static/js/sock.js', + + // LMS + SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx', + AlertStatusBar: './lms/static/js/accessible_components/StatusBarAlert.jsx', + LearnerAnalyticsDashboard: './lms/static/js/learner_analytics_dashboard/LearnerAnalyticsDashboard.jsx', + UpsellExperimentModal: './lms/static/common/js/components/UpsellExperimentModal.jsx', + PortfolioExperimentUpsellModal: './lms/static/common/js/components/PortfolioExperimentUpsellModal.jsx', + EntitlementSupportPage: './lms/djangoapps/support/static/support/jsx/entitlements/index.jsx', + PasswordResetConfirmation: './lms/static/js/student_account/components/PasswordResetConfirmation.jsx', + StudentAccountDeletion: './lms/static/js/student_account/components/StudentAccountDeletion.jsx', + StudentAccountDeletionInitializer: './lms/static/js/student_account/StudentAccountDeletionInitializer.js', + ProblemBrowser: './lms/djangoapps/instructor/static/instructor/ProblemBrowser/index.jsx', + + // Learner Dashboard + EntitlementFactory: './lms/static/js/learner_dashboard/course_entitlement_factory.js', + EntitlementUnenrollmentFactory: './lms/static/js/learner_dashboard/entitlement_unenrollment_factory.js', + ProgramDetailsFactory: './lms/static/js/learner_dashboard/program_details_factory.js', + ProgramListFactory: './lms/static/js/learner_dashboard/program_list_factory.js', + UnenrollmentFactory: './lms/static/js/learner_dashboard/unenrollment_factory.js', + CompletionOnViewService: './lms/static/completion/js/CompletionOnViewService.js', + + // Features + CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js', + CourseHome: './openedx/features/course_experience/static/course_experience/js/CourseHome.js', + CourseOutline: './openedx/features/course_experience/static/course_experience/js/CourseOutline.js', + CourseSock: './openedx/features/course_experience/static/course_experience/js/CourseSock.js', + CourseTalkReviews: './openedx/features/course_experience/static/course_experience/js/CourseTalkReviews.js', + Currency: './openedx/features/course_experience/static/course_experience/js/currency.js', + Enrollment: './openedx/features/course_experience/static/course_experience/js/Enrollment.js', + LatestUpdate: './openedx/features/course_experience/static/course_experience/js/LatestUpdate.js', + WelcomeMessage: './openedx/features/course_experience/static/course_experience/js/WelcomeMessage.js', + + CookiePolicyBanner: './common/static/js/src/CookiePolicyBanner.jsx', + + // Common + ReactRenderer: './common/static/js/src/ReactRenderer.jsx', + XModuleShim: 'xmodule/js/src/xmodule.js', + VerticalStudentView: './common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js', + commons: 'babel-polyfill' + }, + + output: { + path: path.resolve(__dirname, 'common/static/bundles'), + libraryTarget: 'window' + }, + + plugins: [ + new webpack.NoEmitOnErrorsPlugin(), + new webpack.NamedModulesPlugin(), + new BundleTracker({ + path: process.env.STATIC_ROOT_CMS, + filename: 'webpack-stats.json' + }), + new BundleTracker({ + path: process.env.STATIC_ROOT_LMS, + filename: 'webpack-stats.json' + }), + new webpack.ProvidePlugin({ + _: 'underscore', + $: 'jquery', + jQuery: 'jquery', + 'window.jQuery': 'jquery', + Popper: 'popper.js', // used by bootstrap + CodeMirror: 'codemirror', + 'edx.HtmlUtils': 'edx-ui-toolkit/js/utils/html-utils', + AjaxPrefix: 'ajax_prefix', + // This is used by some XModules/XBlocks, which don't have + // any other way to declare that dependency. + $script: 'scriptjs' + }), + + // Note: Until karma-webpack releases v3, it doesn't play well with + // the CommonsChunkPlugin. We have a kludge in karma.common.conf.js + // that dynamically removes this plugin from webpack config when + // running those tests (the details are in that file). This is a + // recommended workaround, as this plugin is just an optimization. But + // because of this, we really don't want to get too fancy with how we + // invoke this plugin until we can upgrade karma-webpack. + new webpack.optimize.CommonsChunkPlugin({ + // If the value below changes, update the render_bundle call in + // common/djangoapps/pipeline_mako/templates/static_content.html + name: 'commons', + filename: 'commons.js', + minChunks: 3 + }) + ], + + module: { + noParse: [ + // See sinon/webpack interaction weirdness: + // https://github.com/webpack/webpack/issues/304#issuecomment-272150177 + // (I've tried every other suggestion solution on that page, this + // was the only one that worked.) + /\/sinon\.js|codemirror-compressed\.js|hls\.js|tinymce\.full\.min\.js/ + ], + rules: [ + { + test: files.namespacedRequire.concat(files.textBangUnderscore, filesWithRequireJSBlocks), + loader: StringReplace.replace( + ['babel-loader'], + { + replacements: [ + { + pattern: defineHeader, + replacement: function() { return ''; } + }, + { + pattern: defineFooter, + replacement: function() { return ''; } + }, + { + pattern: /(\/\* RequireJS) \*\//g, + replacement: function(match, p1) { return p1; } + }, + { + pattern: /\/\* Webpack/g, + replacement: function(match) { return match + ' */'; } + }, + { + pattern: /text!(.*?\.underscore)/g, + replacement: function(match, p1) { return p1; } + }, + { + pattern: /RequireJS.require/g, + replacement: function() { + return 'require'; + } + } + ] + } + ) + }, + { + test: /\.(js|jsx)$/, + exclude: [ + /node_modules/, + files.namespacedRequire, + files.textBangUnderscore, + filesWithRequireJSBlocks + ], + use: 'babel-loader' + }, + { + test: /\.(js|jsx)$/, + include: [ + /paragon/ + ], + use: 'babel-loader' + }, + { + test: path.resolve(__dirname, 'common/static/js/src/ajax_prefix.js'), + use: [ + 'babel-loader', + { + loader: 'exports-loader', + options: { + 'this.AjaxPrefix': true + } + } + ] + }, + { + test: /\.underscore$/, + use: 'raw-loader' + }, + { + // This file is used by both RequireJS and Webpack and depends on window globals + // This is a dirty hack and shouldn't be replicated for other files. + test: path.resolve(__dirname, 'cms/static/cms/js/main.js'), + loader: StringReplace.replace( + ['babel-loader'], + { + replacements: [ + { + pattern: /\(function\(AjaxPrefix\) {/, + replacement: function() { return ''; } + }, + { + pattern: /], function\(domReady, \$, str, Backbone, gettext, NotificationView\) {/, + replacement: function() { + // eslint-disable-next-line + return '], function(domReady, $, str, Backbone, gettext, NotificationView, AjaxPrefix) {'; + } + }, + { + pattern: /'..\/..\/common\/js\/components\/views\/feedback_notification',/, + replacement: function() { + return "'../../common/js/components/views/feedback_notification'," + + "'AjaxPrefix',"; + } + }, + { + pattern: /}\).call\(this, AjaxPrefix\);/, + replacement: function() { return ''; } + }, + { + pattern: /'..\/..\/common\/js\/components\/views\/feedback_notification',/, + replacement: function() { + return "'../../common/js/components/views/feedback_notification'," + + "'AjaxPrefix',"; + } + } + ] + } + ) + }, + { + test: /\.(woff2?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, + loader: 'file-loader' + }, + { + test: /\.svg$/, + loader: 'svg-inline-loader' + }, + { + test: /xblock\/core/, + loader: 'exports-loader?window.XBlock!' + + 'imports-loader?jquery,jquery.immediateDescendents,this=>window' + }, + { + test: /xblock\/runtime.v1/, + loader: 'exports-loader?window.XBlock!imports-loader?XBlock=xblock/core,this=>window' + }, + { + test: /descriptors\/js/, + loader: 'imports-loader?this=>window' + }, + { + test: /modules\/js/, + loader: 'imports-loader?this=>window' + }, + { + test: /codemirror/, + loader: 'exports-loader?window.CodeMirror' + }, + { + test: /tinymce/, + loader: 'imports-loader?this=>window' + }, + { + test: /xmodule\/js\/src\/xmodule/, + loader: 'exports-loader?window.XModule!imports-loader?this=>window' + }, + { + test: /mock-ajax/, + loader: 'imports-loader?exports=>false' + }, + { + test: /d3.min/, + use: [ + 'babel-loader', + { + loader: 'exports-loader', + options: { + d3: true + } + } + ] + }, + { + test: /logger/, + loader: 'imports-loader?this=>window' + } + ] + }, + + resolve: { + extensions: ['.js', '.jsx', '.json'], + alias: { + AjaxPrefix: 'ajax_prefix', + accessibility: 'accessibility_tools', + codemirror: 'codemirror-compressed', + datepair: 'timepicker/datepair', + 'edx-ui-toolkit': 'edx-ui-toolkit/src/', // @TODO: some paths in toolkit are not valid relative paths + ieshim: 'ie_shim', + jquery: 'jquery/src/jquery', // Use the non-diqst form of jQuery for better debugging + optimization + 'jquery.flot': 'flot/jquery.flot.min', + 'jquery.ui': 'jquery-ui.min', + 'jquery.tinymce': 'jquery.tinymce.min', + 'jquery.inputnumber': 'html5-input-polyfills/number-polyfill', + 'jquery.qtip': 'jquery.qtip.min', + 'jquery.smoothScroll': 'jquery.smooth-scroll.min', + 'jquery.timepicker': 'timepicker/jquery.timepicker', + 'backbone.associations': 'backbone-associations/backbone-associations-min', + squire: 'Squire', + tinymce: 'tinymce.full.min', + + // See sinon/webpack interaction weirdness: + // https://github.com/webpack/webpack/issues/304#issuecomment-272150177 + // (I've tried every other suggestion solution on that page, this + // was the only one that worked.) + sinon: __dirname + '/node_modules/sinon/pkg/sinon.js', + hls: 'hls.js/dist/hls.js' + }, + modules: [ + 'cms/djangoapps/pipeline_js/js', + 'cms/static', + 'cms/static/cms/js', + 'cms/templates/js', + 'lms/static', + 'common/lib/xmodule', + 'common/lib/xmodule/xmodule/js/src', + 'common/lib/xmodule/xmodule/assets/word_cloud/src/js', + 'common/static', + 'common/static/coffee/src', + 'common/static/common/js', + 'common/static/common/js/vendor/', + 'common/static/common/js/components', + 'common/static/js/src', + 'common/static/js/vendor/', + 'common/static/js/vendor/jQuery-File-Upload/js/', + 'common/static/js/vendor/tinymce/js/tinymce', + 'node_modules', + 'common/static/xmodule' + ] + }, + + resolveLoader: { + alias: { + text: 'raw-loader' // Compatibility with RequireJSText's text! loader, uses raw-loader under the hood + } + }, + + externals: { + $: 'jQuery', + backbone: 'Backbone', + canvas: 'canvas', + coursetalk: 'CourseTalk', + gettext: 'gettext', + jquery: 'jQuery', + logger: 'Logger', + underscore: '_', + URI: 'URI', + XBlockToXModuleShim: 'XBlockToXModuleShim', + XModule: 'XModule' + }, + + watchOptions: { + poll: true + }, + + node: { + fs: 'empty' + } + + } +}, {web: xmoduleJS}, workerConfig()); -}, xmoduleJS); diff --git a/webpack.dev.config.js b/webpack.dev.config.js index ae22b2bc00..913d773cac 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -5,56 +5,59 @@ var Merge = require('webpack-merge'); var path = require('path'); var webpack = require('webpack'); +var _ = require('underscore'); var commonConfig = require('./webpack.common.config.js'); -module.exports = Merge.smart(commonConfig, { - output: { - filename: '[name].js' - }, - devtool: 'source-map', - plugins: [ - new webpack.LoaderOptionsPlugin({ - debug: true - }), - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('development') - }) - ], - module: { - rules: [ - { - test: /(.scss|.css)$/, - include: [ - /paragon/, - /font-awesome/ - ], - use: [ - 'style-loader', - { - loader: 'css-loader', - options: { - sourceMap: true, - modules: true, - localIdentName: '[name]__[local]' +module.exports = _.values(Merge.smart(commonConfig, { + web: { + output: { + filename: '[name].js' + }, + devtool: 'source-map', + plugins: [ + new webpack.LoaderOptionsPlugin({ + debug: true + }), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('development') + }) + ], + module: { + rules: [ + { + test: /(.scss|.css)$/, + include: [ + /paragon/, + /font-awesome/ + ], + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + sourceMap: true, + modules: true, + localIdentName: '[name]__[local]' + } + }, + { + loader: 'sass-loader', + options: { + data: '$base-rem-size: 0.625; @import "paragon-reset";', + includePaths: [ + path.join(__dirname, './node_modules/@edx/paragon/src/utils'), + path.join(__dirname, './node_modules/') + ], + sourceMap: true + } } - }, - { - loader: 'sass-loader', - options: { - data: '$base-rem-size: 0.625; @import "paragon-reset";', - includePaths: [ - path.join(__dirname, './node_modules/@edx/paragon/src/utils'), - path.join(__dirname, './node_modules/') - ], - sourceMap: true - } - } - ] - } - ] - }, - watchOptions: { - ignored: [/node_modules/, /\.git/] + ] + } + ] + }, + watchOptions: { + ignored: [/node_modules/, /\.git/] + } } -}); +})); diff --git a/webpack.prod.config.js b/webpack.prod.config.js index 97570626b2..360ab56d4d 100644 --- a/webpack.prod.config.js +++ b/webpack.prod.config.js @@ -4,10 +4,13 @@ var Merge = require('webpack-merge'); var webpack = require('webpack'); +var BundleTracker = require('webpack-bundle-tracker'); +var _ = require('underscore'); var commonConfig = require('./webpack.common.config.js'); var optimizedConfig = Merge.smart(commonConfig, { + web: { output: { filename: '[name].[chunkhash].js' }, @@ -28,7 +31,7 @@ var optimizedConfig = Merge.smart(commonConfig, { minChunks: 3 }) ] -}); +}}); // requireCompatConfig only exists so that you can use RequireJS to require a // Webpack bundle (but try not to do that if you can help it). RequireJS knows @@ -44,6 +47,7 @@ var optimizedConfig = Merge.smart(commonConfig, { // Step 1: Alter the bundle output names to omit the chunkhash. var requireCompatConfig = Merge.smart(optimizedConfig, { + web: { output: { filename: '[name].js' }, @@ -56,14 +60,12 @@ var requireCompatConfig = Merge.smart(optimizedConfig, { minChunks: 3 }) ] -}); +}}); // Step 2: Remove the plugin entries that generate the webpack-stats.json files // that Django needs to look up resources. We never want to accidentally // overwrite those because it means that we'll be serving assets with shorter // cache times. RequireJS never looks at the webpack-stats.json file. -requireCompatConfig.plugins = requireCompatConfig.plugins.filter( - function(plugin) { return !plugin.options || (plugin.options && plugin.options.filename !== 'webpack-stats.json'); } -); +requireCompatConfig.web.plugins = requireCompatConfig.web.plugins.filter((plugin) => !(plugin instanceof BundleTracker)); -module.exports = [optimizedConfig, requireCompatConfig]; +module.exports = [..._.values(optimizedConfig), ..._.values(requireCompatConfig)];