Enable course run level overrides for proctoring configuration.

This commit is contained in:
Michael Roytman
2018-11-02 11:41:26 -04:00
committed by Dave St.Germain
parent 77922ae073
commit ecabcf90dd
22 changed files with 627 additions and 90 deletions

View File

@@ -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
)

View File

@@ -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:

View File

@@ -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,

View File

@@ -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)

View File

@@ -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 ####################

View File

@@ -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 #####################

View File

@@ -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.

View File

@@ -36,6 +36,7 @@ from lms.envs.test import (
JWT_AUTH,
REGISTRATION_EXTRA_FIELDS,
ECOMMERCE_API_URL,
PROCTORING_BACKENDS,
)

View File

@@ -23,6 +23,7 @@ DATABASES = {
}
######################### PIPELINE ####################################
# Use RequireJS optimized storage

View File

@@ -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=_(

View File

@@ -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, {})

View File

@@ -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 ####################

View File

@@ -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

View File

@@ -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', {})

View File

@@ -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

View File

@@ -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 ####################################

View File

@@ -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 = (

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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