Enable course run level overrides for proctoring configuration.
This commit is contained in:
committed by
Dave St.Germain
parent
77922ae073
commit
ecabcf90dd
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,50 @@ 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_configuration.get('backend', None),
|
||||
}
|
||||
|
||||
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,
|
||||
'rules': course.proctoring_configuration.get('rules', None)
|
||||
}
|
||||
|
||||
# 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
|
||||
)
|
||||
review_policy_has_rules = exam_review_policy_metadata.get('rules', None)
|
||||
|
||||
if timed_exam.exam_review_rules or review_policy_has_rules: # won't save an empty rule.
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,11 +28,17 @@ class TestProctoredExams(ModuleStoreTestCase):
|
||||
"""
|
||||
super(TestProctoredExams, self).setUp()
|
||||
|
||||
default_proctoring_provider = settings.PROCTORING_BACKENDS['DEFAULT']
|
||||
|
||||
self.course = CourseFactory.create(
|
||||
org='edX',
|
||||
course='900',
|
||||
run='test_run',
|
||||
enable_proctored_exams=True
|
||||
enable_proctored_exams=True,
|
||||
proctoring_configuration={
|
||||
'backend': default_proctoring_provider,
|
||||
'rules': settings.PROCTORING_BACKENDS[default_proctoring_provider]['default_rules'],
|
||||
}
|
||||
)
|
||||
|
||||
def _verify_exam_data(self, sequence, expected_active):
|
||||
@@ -49,6 +56,7 @@ class TestProctoredExams(ModuleStoreTestCase):
|
||||
# get the review policy object
|
||||
exam_review_policy = get_review_policy_by_exam_id(exam['id'])
|
||||
self.assertEqual(exam_review_policy['review_policy'], sequence.exam_review_rules)
|
||||
self.assertEqual(exam_review_policy['rules'], self.course.proctoring_configuration['rules'])
|
||||
|
||||
if not exam['is_proctored'] and not exam['is_practice_exam']:
|
||||
# the hide after due value only applies to timed exams
|
||||
@@ -61,20 +69,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_configuration['backend'])
|
||||
|
||||
@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 +92,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 +114,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 +187,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,
|
||||
|
||||
@@ -10,6 +10,7 @@ from xblock_django.models import XBlockStudioConfigurationFlag
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
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 +21,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 +66,71 @@ 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_configuration')
|
||||
|
||||
# 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')
|
||||
return black_list
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, descriptor):
|
||||
@@ -133,8 +140,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 +178,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 +214,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 +248,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)
|
||||
|
||||
@@ -492,7 +492,6 @@ XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.g
|
||||
|
||||
################# PROCTORING CONFIGURATION ##################
|
||||
|
||||
PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER)
|
||||
PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS)
|
||||
|
||||
################# MICROSITE ####################
|
||||
|
||||
@@ -1446,11 +1446,12 @@ MICROSITE_TEMPLATE_BACKEND = 'microsite_configuration.backends.filebased.Filebas
|
||||
MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = 5 * 60
|
||||
|
||||
############################### PROCTORING CONFIGURATION DEFAULTS ##############
|
||||
PROCTORING_BACKEND_PROVIDER = {
|
||||
'class': 'edx_proctoring.backends.null.NullBackendProvider',
|
||||
'options': {},
|
||||
}
|
||||
PROCTORING_SETTINGS = {}
|
||||
PROCTORING_BACKENDS = {
|
||||
'DEFAULT': 'null',
|
||||
'null': {},
|
||||
}
|
||||
|
||||
|
||||
############################ Global Database Configuration #####################
|
||||
|
||||
|
||||
@@ -495,8 +495,8 @@ XBLOCK_SETTINGS.setdefault("VideoModule", {})['YOUTUBE_API_KEY'] = AUTH_TOKENS.g
|
||||
|
||||
################# PROCTORING CONFIGURATION ##################
|
||||
|
||||
PROCTORING_BACKEND_PROVIDER = AUTH_TOKENS.get("PROCTORING_BACKEND_PROVIDER", PROCTORING_BACKEND_PROVIDER)
|
||||
PROCTORING_SETTINGS = ENV_TOKENS.get("PROCTORING_SETTINGS", PROCTORING_SETTINGS)
|
||||
PROCTORING_BACKENDS = ENV_TOKENS.get("PROCTORING_BACKENDS", PROCTORING_BACKENDS)
|
||||
|
||||
################# MICROSITE ####################
|
||||
# microsite specific configurations.
|
||||
|
||||
@@ -36,6 +36,7 @@ from lms.envs.test import (
|
||||
JWT_AUTH,
|
||||
REGISTRATION_EXTRA_FIELDS,
|
||||
ECOMMERCE_API_URL,
|
||||
PROCTORING_BACKENDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ DATABASES = {
|
||||
|
||||
}
|
||||
|
||||
|
||||
######################### PIPELINE ####################################
|
||||
|
||||
# Use RequireJS optimized storage
|
||||
|
||||
@@ -16,7 +16,7 @@ from openedx.core.djangoapps.video_pipeline.models import VideoUploadsEnabledByD
|
||||
from openedx.core.lib.license import LicenseMixin
|
||||
from path import Path as path
|
||||
from pytz import utc
|
||||
from six import text_type
|
||||
from six import text_type, iteritems
|
||||
from xblock.fields import Scope, List, String, Dict, Boolean, Integer, Float
|
||||
|
||||
from xmodule import course_metadata_utils
|
||||
@@ -183,6 +183,151 @@ class TextbookList(List):
|
||||
return json_data
|
||||
|
||||
|
||||
class ProctoringConfiguration(Dict):
|
||||
def from_json(self, value):
|
||||
"""
|
||||
Return ProctoringConfiguration as full featured Python type. Perform validation on the backend
|
||||
and the rules and include any inherited values from the platform default.
|
||||
"""
|
||||
errors = []
|
||||
value = super(ProctoringConfiguration, self).from_json(value)
|
||||
proctoring_backend_settings = getattr(
|
||||
settings,
|
||||
'PROCTORING_BACKENDS',
|
||||
None
|
||||
)
|
||||
|
||||
backend_errors = self._validate_proctoring_backend(value, proctoring_backend_settings)
|
||||
rules_errors = self._validate_proctoring_rules(value, proctoring_backend_settings)
|
||||
|
||||
errors.extend(backend_errors)
|
||||
errors.extend(rules_errors)
|
||||
|
||||
if errors:
|
||||
raise ValueError(errors)
|
||||
|
||||
value = self._get_proctoring_value(value, proctoring_backend_settings)
|
||||
|
||||
return value
|
||||
|
||||
def _get_proctoring_value(self, value, proctoring_backend_settings):
|
||||
"""
|
||||
Return a proctoring value that includes any inherited attributes from the platform defaults
|
||||
for the backend or rules.
|
||||
"""
|
||||
proctoring_provider = value.get('backend', None)
|
||||
proctoring_provider_rules = value.get('rules', None)
|
||||
|
||||
# if both are missing from the value, return the default
|
||||
if proctoring_provider is None and proctoring_provider_rules is None:
|
||||
return self.default
|
||||
|
||||
# if provider is missing, but rules are not, use the default provider
|
||||
if proctoring_provider is None and proctoring_provider_rules is not None:
|
||||
value['backend'] = proctoring_backend_settings.get('DEFAULT', None)
|
||||
|
||||
# if rules are missing, but provider is not, use the default rules for the provider
|
||||
if proctoring_provider is not None and proctoring_provider_rules is None:
|
||||
value['rules'] = proctoring_backend_settings.get(value['backend'], {}).get('default_rules', {})
|
||||
|
||||
proctoring_provider_rules_set = (
|
||||
proctoring_backend_settings
|
||||
.get(proctoring_provider, {})
|
||||
.get('default_rules', {})
|
||||
)
|
||||
proctoring_provider_rules = value.get('rules', None)
|
||||
|
||||
# add back in any missing rules for the provider
|
||||
for default_rule, is_enabled in iteritems(proctoring_provider_rules_set):
|
||||
if default_rule not in proctoring_provider_rules:
|
||||
proctoring_provider_rules[default_rule] = is_enabled
|
||||
return value
|
||||
|
||||
def _validate_proctoring_backend(self, value, proctoring_backend_settings):
|
||||
"""
|
||||
Validate the value for the proctoring backend. If the proctoring backend value is
|
||||
specified, and it is not one of the backends configured at the platform level, return
|
||||
a list of error messages to the caller.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
proctoring_provider_whitelist = [provider for provider in proctoring_backend_settings if provider != 'DEFAULT']
|
||||
proctoring_provider_whitelist.sort()
|
||||
proctoring_provider = value.get('backend', None)
|
||||
|
||||
if proctoring_provider and proctoring_provider not in proctoring_provider_whitelist:
|
||||
errors.append(
|
||||
_('The selected proctoring backend, {proctoring_backend}, is not a valid backend. '
|
||||
'Please select from one of {available_backends}.')
|
||||
.format(
|
||||
proctoring_backend=proctoring_provider,
|
||||
available_backends=proctoring_provider_whitelist
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
def _validate_proctoring_rules(self, value, proctoring_backend_settings):
|
||||
"""
|
||||
Validate the value for the proctoring rules. If the proctoring rules value is
|
||||
specified, and it is not one of the rules configured for the corresponding backend
|
||||
at the platform level, or if the value for the rule is not a boolean,
|
||||
return a list of error messages to the caller.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
proctoring_provider = value.get('backend', None)
|
||||
proctoring_provider_rules = value.get('rules', None)
|
||||
proctoring_provider_rules_set = (
|
||||
proctoring_backend_settings
|
||||
.get(proctoring_provider, {})
|
||||
.get('default_rules', None)
|
||||
)
|
||||
|
||||
if proctoring_provider_rules:
|
||||
for rule, is_enabled in iteritems(proctoring_provider_rules):
|
||||
if not isinstance(is_enabled, bool):
|
||||
errors.append(
|
||||
_('The value for proctoring configuration rule {rule} '
|
||||
'should be either true or false.')
|
||||
.format(rule=rule)
|
||||
)
|
||||
if not proctoring_provider_rules_set or rule not in proctoring_provider_rules_set:
|
||||
errors.append(
|
||||
_('The proctoring configuration rule {rule} '
|
||||
'is not a valid rule for provider {provider}.')
|
||||
.format(
|
||||
rule=rule,
|
||||
provider=proctoring_provider
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
"""
|
||||
Return default value for ProctoringConfiguration.
|
||||
"""
|
||||
default = super(ProctoringConfiguration, self).default
|
||||
|
||||
proctoring_backend_settings = getattr(settings, 'PROCTORING_BACKENDS', None)
|
||||
|
||||
if proctoring_backend_settings:
|
||||
default_proctoring_provider = proctoring_backend_settings.get('DEFAULT', None)
|
||||
|
||||
try:
|
||||
default_proctoring_rules = proctoring_backend_settings[default_proctoring_provider]['default_rules']
|
||||
except KeyError:
|
||||
default_proctoring_rules = {}
|
||||
|
||||
return {
|
||||
'backend': default_proctoring_provider,
|
||||
'rules': default_proctoring_rules,
|
||||
}
|
||||
return default
|
||||
|
||||
|
||||
class CourseFields(object):
|
||||
lti_passports = List(
|
||||
display_name=_("LTI Passports"),
|
||||
@@ -738,6 +883,12 @@ class CourseFields(object):
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
proctoring_configuration = ProctoringConfiguration(
|
||||
display_name=_("Proctoring Configuration"),
|
||||
help=_("Enter a proctoring configuration."),
|
||||
scope=Scope.settings,
|
||||
)
|
||||
|
||||
allow_proctoring_opt_out = Boolean(
|
||||
display_name=_("Allow Opting Out of Proctored Exams"),
|
||||
help=_(
|
||||
|
||||
@@ -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,282 @@ 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 ProctoringConfigurationTestCase(unittest.TestCase):
|
||||
"""
|
||||
Tests for ProctoringConfiguration, including the default value, validation, and inheritance behavior.
|
||||
"""
|
||||
shard = 1
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Initialize dummy testing course.
|
||||
"""
|
||||
super(ProctoringConfigurationTestCase, self).setUp()
|
||||
self.proctoring_configuration = xmodule.course_module.ProctoringConfiguration()
|
||||
|
||||
def test_from_json_with_platform_default(self):
|
||||
"""
|
||||
Test that a proctoring configuration value equivalent to the platform
|
||||
default will pass validation.
|
||||
"""
|
||||
default_provider = settings.PROCTORING_BACKENDS.get('DEFAULT')
|
||||
|
||||
value = {
|
||||
'backend': default_provider,
|
||||
'rules': settings.PROCTORING_BACKENDS[default_provider]['default_rules'],
|
||||
}
|
||||
|
||||
# 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_configuration.from_json(value), value)
|
||||
|
||||
@override_settings(
|
||||
PROCTORING_BACKENDS={
|
||||
'DEFAULT': 'mock_proctoring_without_rules',
|
||||
'mock': {
|
||||
'default_rules': {
|
||||
'allow_snarfing': True,
|
||||
'allow_grok': False
|
||||
}
|
||||
},
|
||||
'mock_proctoring_without_rules': {}
|
||||
}
|
||||
)
|
||||
def test_from_json_with_provider_with_rules(self):
|
||||
"""
|
||||
Test that a proctoring provider with rules other than the platform default
|
||||
passes validation.
|
||||
"""
|
||||
provider = 'mock'
|
||||
value = {
|
||||
'backend': provider,
|
||||
'rules': settings.PROCTORING_BACKENDS[provider]['default_rules'],
|
||||
}
|
||||
|
||||
# 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_configuration.from_json(value), value)
|
||||
|
||||
def test_from_json_with_provider_without_rules(self):
|
||||
"""
|
||||
Test that a proctoring provider without rules passes validation.
|
||||
"""
|
||||
value = {
|
||||
'backend': 'mock_proctoring_without_rules',
|
||||
'rules': {},
|
||||
}
|
||||
|
||||
# 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_configuration.from_json(value), value)
|
||||
|
||||
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']
|
||||
|
||||
value = {
|
||||
'backend': provider,
|
||||
'rules': {},
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError) as context_manager:
|
||||
self.proctoring_configuration.from_json(value)
|
||||
self.assertEqual(
|
||||
context_manager.exception.args[0],
|
||||
['The selected proctoring backend, {}, is not a valid backend. Please select from one of {}.'
|
||||
.format(provider, proctoring_provider_whitelist)]
|
||||
)
|
||||
|
||||
def test_from_json_with_invalid_rules(self):
|
||||
"""
|
||||
Test that an invalid rule (i.e. not one configured at the platform level) for a
|
||||
valid provider throws a ValueError with the correct error message.
|
||||
"""
|
||||
provider = 'mock'
|
||||
rules = settings.PROCTORING_BACKENDS[provider]['default_rules'].copy()
|
||||
rules['allow_foo'] = True
|
||||
|
||||
value = {
|
||||
'backend': provider,
|
||||
'rules': rules,
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError) as context_manager:
|
||||
self.proctoring_configuration.from_json(value)
|
||||
self.assertEqual(
|
||||
context_manager.exception.args[0],
|
||||
['The proctoring configuration rule {} is not a valid rule for provider {}.'.
|
||||
format('allow_foo', provider)]
|
||||
)
|
||||
|
||||
def test_from_json_with_invalid_rule_value(self):
|
||||
"""
|
||||
Test that an invalid rule value (i.e. not a boolean) for a valid rule for a
|
||||
valid provider throws a ValueError with the correct error message.
|
||||
"""
|
||||
provider = 'mock'
|
||||
rules = settings.PROCTORING_BACKENDS[provider]['default_rules'].copy()
|
||||
rules['allow_grok'] = 'yes'
|
||||
|
||||
value = {
|
||||
'backend': provider,
|
||||
'rules': rules,
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError) as context_manager:
|
||||
self.proctoring_configuration.from_json(value)
|
||||
self.assertEqual(
|
||||
context_manager.exception.args[0],
|
||||
['The value for proctoring configuration rule {} should be either true or false.'.
|
||||
format('allow_grok')]
|
||||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
provider = 'mock'
|
||||
|
||||
value = {
|
||||
'rules': {}
|
||||
}
|
||||
|
||||
expected_value = value.copy()
|
||||
expected_value['backend'] = provider
|
||||
|
||||
self.assertEqual(self.proctoring_configuration.from_json(value), expected_value)
|
||||
|
||||
def test_from_json_adds_platform_defaults_for_missing_rules(self):
|
||||
"""
|
||||
Test that a value with no rules will inherit the default rules for
|
||||
that provider from the platform defaults.
|
||||
"""
|
||||
provider = 'mock'
|
||||
|
||||
value = {
|
||||
'backend': provider
|
||||
}
|
||||
|
||||
expected_value = value.copy()
|
||||
expected_value['rules'] = settings.PROCTORING_BACKENDS[provider]['default_rules']
|
||||
|
||||
self.assertEqual(self.proctoring_configuration.from_json(value), expected_value)
|
||||
|
||||
def test_from_json_adds_platform_defaults_for_missing_rules_no_rules_as_empty_dict(self):
|
||||
"""
|
||||
Test that a value with no rules will inherit an empty dict for
|
||||
a provider without rules in the platform defaults.
|
||||
"""
|
||||
provider = 'mock_proctoring_without_rules'
|
||||
|
||||
value = {
|
||||
'backend': provider
|
||||
}
|
||||
|
||||
expected_value = value.copy()
|
||||
expected_value['rules'] = {}
|
||||
|
||||
self.assertEqual(self.proctoring_configuration.from_json(value), expected_value)
|
||||
|
||||
def test_from_json_adds_platform_defaults_for_missing_provider_and_rules(self):
|
||||
"""
|
||||
Test that a value with no rules and no provider will inherit the platform
|
||||
defaults.
|
||||
"""
|
||||
self.assertEqual(self.proctoring_configuration.from_json({}), self.proctoring_configuration.default)
|
||||
|
||||
def test_from_json_adds_missing_rules_from_platform_default(self):
|
||||
"""
|
||||
Test that a value that is missing rules present in the default will
|
||||
inherit these rules from the platform default.
|
||||
"""
|
||||
provider = 'mock'
|
||||
rules = settings.PROCTORING_BACKENDS[provider]['default_rules'].copy()
|
||||
del rules['allow_snarfing']
|
||||
|
||||
value = {
|
||||
'backend': provider,
|
||||
'rules': rules,
|
||||
}
|
||||
|
||||
expected_value = value.copy()
|
||||
expected_value['rules'] = settings.PROCTORING_BACKENDS[provider]['default_rules']
|
||||
|
||||
self.assertEqual(self.proctoring_configuration.from_json(value), expected_value)
|
||||
|
||||
@override_settings(
|
||||
PROCTORING_BACKENDS={
|
||||
'mock': {
|
||||
'default_rules': {
|
||||
'allow_snarfing': True,
|
||||
'allow_grok': False
|
||||
}
|
||||
},
|
||||
'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.
|
||||
"""
|
||||
expected_default = {
|
||||
'backend': None,
|
||||
'rules': {}
|
||||
}
|
||||
|
||||
self. assertEqual(self.proctoring_configuration.default, expected_default)
|
||||
|
||||
def test_default_with_platform_default_with_rules(self):
|
||||
"""
|
||||
Test that, when the platform default provider with rules is specified, the default is correct.
|
||||
"""
|
||||
default_provider = settings.PROCTORING_BACKENDS.get('DEFAULT')
|
||||
default_rules = settings.PROCTORING_BACKENDS[default_provider]['default_rules']
|
||||
|
||||
expected_default = {
|
||||
'backend': default_provider,
|
||||
'rules': default_rules
|
||||
}
|
||||
|
||||
self.assertEqual(self.proctoring_configuration.default, expected_default)
|
||||
|
||||
@override_settings(
|
||||
PROCTORING_BACKENDS={
|
||||
'DEFAULT': 'mock_proctoring_without_rules',
|
||||
'mock': {
|
||||
'default_rules': {
|
||||
'allow_snarfing': True,
|
||||
'allow_grok': False
|
||||
}
|
||||
},
|
||||
'mock_proctoring_without_rules': {}
|
||||
}
|
||||
)
|
||||
def test_default_with_platform_default_without_rules(self):
|
||||
"""
|
||||
Test that, when the platform default provider without rules is specified, the default is correct.
|
||||
"""
|
||||
default_provider = 'mock_proctoring_without_rules'
|
||||
default_rules = {}
|
||||
|
||||
expected_default = {
|
||||
'backend': default_provider,
|
||||
'rules': default_rules
|
||||
}
|
||||
|
||||
self.assertEqual(self.proctoring_configuration.default, expected_default)
|
||||
|
||||
@override_settings(PROCTORING_BACKENDS=None)
|
||||
def test_default_default_with_no_platform_default(self):
|
||||
"""
|
||||
Test that, when the platform default is not specified, the default is correct.
|
||||
"""
|
||||
default = self.proctoring_configuration.default
|
||||
self.assertEqual(default, {})
|
||||
|
||||
@@ -894,7 +894,6 @@ 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 ####################
|
||||
|
||||
@@ -3217,11 +3217,11 @@ RSS_PROXY_CACHE_TIMEOUT = 3600 # The length of time we cache RSS retrieved from
|
||||
|
||||
#### PROCTORING CONFIGURATION DEFAULTS
|
||||
|
||||
PROCTORING_BACKEND_PROVIDER = {
|
||||
'class': 'edx_proctoring.backends.null.NullBackendProvider',
|
||||
'options': {},
|
||||
}
|
||||
PROCTORING_SETTINGS = {}
|
||||
PROCTORING_BACKENDS = {
|
||||
'DEFAULT': 'null',
|
||||
'null': {},
|
||||
}
|
||||
|
||||
#### Custom Courses for EDX (CCX) configuration
|
||||
|
||||
|
||||
@@ -890,8 +890,8 @@ 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)
|
||||
PROCTORING_BACKENDS = ENV_TOKENS.get("PROCTORING_BACKENDS", PROCTORING_BACKENDS)
|
||||
|
||||
################# MICROSITE ####################
|
||||
MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {})
|
||||
|
||||
@@ -124,6 +124,17 @@ XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
|
||||
MOCK_STAFF_GRADING = True
|
||||
MOCK_PEER_GRADING = True
|
||||
|
||||
PROCTORING_BACKENDS = {
|
||||
'DEFAULT': 'mock',
|
||||
'mock': {
|
||||
'default_rules': {
|
||||
'allow_snarfing': True,
|
||||
'allow_grok': False
|
||||
}
|
||||
},
|
||||
'mock_proctoring_without_rules': {}
|
||||
}
|
||||
|
||||
############################ STATIC FILES #############################
|
||||
|
||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things
|
||||
|
||||
@@ -35,6 +35,16 @@ XQUEUE_INTERFACE = {
|
||||
"basic_auth": ('anant', 'agarwal'),
|
||||
}
|
||||
|
||||
PROCTORING_BACKENDS = {
|
||||
'DEFAULT': 'mock',
|
||||
'mock': {
|
||||
'default_rules': {
|
||||
'allow_snarfing': True,
|
||||
'allow_grok': False,
|
||||
}
|
||||
},
|
||||
'mock_proctoring_without_rules': {},
|
||||
}
|
||||
|
||||
######################### PIPELINE ####################################
|
||||
|
||||
|
||||
@@ -51,6 +51,17 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
PROCTORING_BACKENDS = {
|
||||
'DEFAULT': 'mock',
|
||||
'mock': {
|
||||
'default_rules': {
|
||||
'allow_snarfing': True,
|
||||
'allow_grok': False,
|
||||
}
|
||||
},
|
||||
'mock_proctoring_without_rules': {},
|
||||
}
|
||||
|
||||
FEATURES = {}
|
||||
|
||||
INSTALLED_APPS = (
|
||||
|
||||
@@ -80,7 +80,7 @@ edx-enterprise
|
||||
edx-milestones
|
||||
edx-oauth2-provider
|
||||
edx-organizations
|
||||
edx-proctoring
|
||||
edx-proctoring==1.5.0b1
|
||||
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
|
||||
|
||||
@@ -124,7 +124,11 @@ edx-milestones==0.1.13
|
||||
edx-oauth2-provider==1.2.2
|
||||
edx-opaque-keys[django]==0.4.4
|
||||
edx-organizations==1.0.0
|
||||
<<<<<<< HEAD
|
||||
edx-proctoring==1.4.0
|
||||
=======
|
||||
edx-proctoring==1.5.0b1
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
edx-rest-api-client==1.9.2
|
||||
edx-search==1.2.1
|
||||
edx-submissions==2.0.12
|
||||
@@ -145,7 +149,11 @@ hash-ring==1.3.1 # via django-memcached-hashring
|
||||
help-tokens==1.0.3
|
||||
html5lib==1.0.1
|
||||
httplib2==0.12.0 # via oauth2, zendesk
|
||||
<<<<<<< HEAD
|
||||
idna==2.8
|
||||
=======
|
||||
idna==2.7
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
ipaddr==2.1.11
|
||||
ipaddress==1.0.22
|
||||
isodate==0.6.0 # via python-saml
|
||||
@@ -169,7 +177,11 @@ mock==1.0.1
|
||||
mongoengine==0.10.0
|
||||
mysql-python==1.2.5
|
||||
networkx==1.7
|
||||
<<<<<<< HEAD
|
||||
newrelic==4.8.0.110
|
||||
=======
|
||||
newrelic==4.6.0.106
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
nltk==3.4
|
||||
nodeenv==1.1.1
|
||||
numpy==1.6.2
|
||||
@@ -204,13 +216,19 @@ 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
|
||||
<<<<<<< HEAD
|
||||
reportlab==3.5.12
|
||||
requests-oauthlib==1.0.0
|
||||
requests==2.21.0
|
||||
=======
|
||||
reportlab==3.5.11
|
||||
requests-oauthlib==1.0.0
|
||||
requests==2.20.1
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
rest-condition==1.0.3
|
||||
rfc6266-parser==0.0.5.post2
|
||||
rules==2.0.1
|
||||
|
||||
@@ -144,7 +144,11 @@ edx-milestones==0.1.13
|
||||
edx-oauth2-provider==1.2.2
|
||||
edx-opaque-keys[django]==0.4.4
|
||||
edx-organizations==1.0.0
|
||||
<<<<<<< HEAD
|
||||
edx-proctoring==1.4.0
|
||||
=======
|
||||
edx-proctoring==1.5.0b1
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
edx-rest-api-client==1.9.2
|
||||
edx-search==1.2.1
|
||||
edx-sphinx-theme==1.4.0
|
||||
@@ -217,7 +221,11 @@ more-itertools==4.3.0
|
||||
moto==0.3.1
|
||||
mysql-python==1.2.5
|
||||
networkx==1.7
|
||||
<<<<<<< HEAD
|
||||
newrelic==4.8.0.110
|
||||
=======
|
||||
newrelic==4.6.0.106
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
nltk==3.4
|
||||
nodeenv==1.1.1
|
||||
numpy==1.6.2
|
||||
@@ -281,15 +289,21 @@ 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
|
||||
radon==2.4.0
|
||||
redis==2.10.6
|
||||
<<<<<<< HEAD
|
||||
reportlab==3.5.12
|
||||
requests-oauthlib==1.0.0
|
||||
requests==2.21.0
|
||||
=======
|
||||
reportlab==3.5.11
|
||||
requests-oauthlib==1.0.0
|
||||
requests==2.20.1
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
rest-condition==1.0.3
|
||||
rfc6266-parser==0.0.5.post2
|
||||
rules==2.0.1
|
||||
|
||||
@@ -22,7 +22,11 @@ psutil==1.2.1
|
||||
pymongo==2.9.1
|
||||
python-memcached==1.48
|
||||
pyyaml==3.13 # via watchdog
|
||||
<<<<<<< HEAD
|
||||
requests==2.21.0
|
||||
=======
|
||||
requests==2.20.1
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
six==1.11.0 # via edx-opaque-keys, libsass, paver, stevedore
|
||||
stevedore==1.10.0
|
||||
urllib3==1.23 # via requests
|
||||
|
||||
@@ -139,7 +139,11 @@ edx-milestones==0.1.13
|
||||
edx-oauth2-provider==1.2.2
|
||||
edx-opaque-keys[django]==0.4.4
|
||||
edx-organizations==1.0.0
|
||||
<<<<<<< HEAD
|
||||
edx-proctoring==1.4.0
|
||||
=======
|
||||
edx-proctoring==1.5.0b1
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
edx-rest-api-client==1.9.2
|
||||
edx-search==1.2.1
|
||||
edx-submissions==2.0.12
|
||||
@@ -209,7 +213,11 @@ more-itertools==4.3.0 # via pytest
|
||||
moto==0.3.1
|
||||
mysql-python==1.2.5
|
||||
networkx==1.7
|
||||
<<<<<<< HEAD
|
||||
newrelic==4.8.0.110
|
||||
=======
|
||||
newrelic==4.6.0.106
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
nltk==3.4
|
||||
nodeenv==1.1.1
|
||||
numpy==1.6.2
|
||||
@@ -270,15 +278,21 @@ 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
|
||||
radon==2.4.0
|
||||
redis==2.10.6
|
||||
<<<<<<< HEAD
|
||||
reportlab==3.5.12
|
||||
requests-oauthlib==1.0.0
|
||||
requests==2.21.0
|
||||
=======
|
||||
reportlab==3.5.11
|
||||
requests-oauthlib==1.0.0
|
||||
requests==2.20.1
|
||||
>>>>>>> Enable course run level overrides for proctoring configuration.
|
||||
rest-condition==1.0.3
|
||||
rfc6266-parser==0.0.5.post2
|
||||
rules==2.0.1
|
||||
|
||||
Reference in New Issue
Block a user