Commit a4b172d9 by cahrens

Display names, help text, and "deprecated" for Advanced Settings.

STUD-302, STUD-303
parent 3c838081
...@@ -7,6 +7,9 @@ the top. Include a label indicating the component affected. ...@@ -7,6 +7,9 @@ the top. Include a label indicating the component affected.
Blades: Redirect Chinese students to a Chinese CDN for video. BLD-1052. Blades: Redirect Chinese students to a Chinese CDN for video. BLD-1052.
Studio: Show display names and help text in Advanced Settings. Also hide deprecated settings
by default.
Studio: Move Peer Assessment into advanced problems menu. Studio: Move Peer Assessment into advanced problems menu.
Studio: Support creation and editing of split_test instances (Content Experiments) Studio: Support creation and editing of split_test instances (Content Experiments)
......
...@@ -36,7 +36,7 @@ Feature: CMS.Advanced (manual) course policy ...@@ -36,7 +36,7 @@ Feature: CMS.Advanced (manual) course policy
@skip_sauce @skip_sauce
Scenario: Test how multi-line input appears Scenario: Test how multi-line input appears
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value for "discussion_topics" When I create a JSON object as a value for "Discussion Topic Mapping"
Then it is displayed as formatted Then it is displayed as formatted
And I reload the page And I reload the page
Then it is displayed as formatted Then it is displayed as formatted
...@@ -45,7 +45,7 @@ Feature: CMS.Advanced (manual) course policy ...@@ -45,7 +45,7 @@ Feature: CMS.Advanced (manual) course policy
@skip_sauce @skip_sauce
Scenario: Test error if value supplied is of the wrong type Scenario: Test error if value supplied is of the wrong type
Given I am on the Advanced Course Settings page in Studio Given I am on the Advanced Course Settings page in Studio
When I create a JSON object as a value for "display_name" When I create a JSON object as a value for "Course Display Name"
Then I get an error on save Then I get an error on save
And I reload the page And I reload the page
Then the policy key value is unchanged Then the policy key value is unchanged
...@@ -67,3 +67,14 @@ Feature: CMS.Advanced (manual) course policy ...@@ -67,3 +67,14 @@ Feature: CMS.Advanced (manual) course policy
When I edit the value of a policy key When I edit the value of a policy key
And I press the "Save" notification button And I press the "Save" notification button
Then I see a confirmation that my changes have been saved Then I see a confirmation that my changes have been saved
Scenario: Deprecated Settings are not shown by default
Given I am on the Advanced Course Settings page in Studio
Then deprecated settings are not shown
Scenario: Deprecated Settings can be toggled
Given I am on the Advanced Course Settings page in Studio
When I toggle the display of deprecated settings
Then deprecated settings are then shown
And I toggle the display of deprecated settings
Then deprecated settings are not shown
...@@ -5,9 +5,12 @@ from lettuce import world, step ...@@ -5,9 +5,12 @@ from lettuce import world, step
from nose.tools import assert_false, assert_equal, assert_regexp_matches # pylint: disable=E0611 from nose.tools import assert_false, assert_equal, assert_regexp_matches # pylint: disable=E0611
from common import type_in_codemirror, press_the_notification_button, get_codemirror_value from common import type_in_codemirror, press_the_notification_button, get_codemirror_value
KEY_CSS = '.key input.policy-key' KEY_CSS = '.key h3.title'
DISPLAY_NAME_KEY = "display_name" DISPLAY_NAME_KEY = "Course Display Name"
DISPLAY_NAME_VALUE = '"Robot Super Course"' DISPLAY_NAME_VALUE = '"Robot Super Course"'
ADVANCED_MODULES_KEY = "Advanced Module List"
# A few deprecated settings for testing toggling functionality.
DEPRECATED_SETTINGS = ["CSS Class for Course Reruns", "Hide Progress Tab", "XQA Key"]
@step('I select the Advanced Settings$') @step('I select the Advanced Settings$')
...@@ -58,7 +61,7 @@ def create_value_not_in_quotes(step): ...@@ -58,7 +61,7 @@ def create_value_not_in_quotes(step):
def i_see_default_advanced_settings(step): def i_see_default_advanced_settings(step):
# Test only a few of the existing properties (there are around 34 of them) # Test only a few of the existing properties (there are around 34 of them)
assert_policy_entries( assert_policy_entries(
["advanced_modules", DISPLAY_NAME_KEY, "show_calculator"], ["[]", DISPLAY_NAME_VALUE, "false"]) [ADVANCED_MODULES_KEY, DISPLAY_NAME_KEY, "Show Calculator"], ["[]", DISPLAY_NAME_VALUE, "false"])
@step('the settings are alphabetized$') @step('the settings are alphabetized$')
...@@ -73,12 +76,15 @@ def they_are_alphabetized(step): ...@@ -73,12 +76,15 @@ def they_are_alphabetized(step):
@step('it is displayed as formatted$') @step('it is displayed as formatted$')
def it_is_formatted(step): def it_is_formatted(step):
assert_policy_entries(['discussion_topics'], ['{\n "key": "value",\n "key_2": "value_2"\n}']) assert_policy_entries(['Discussion Topic Mapping'], ['{\n "key": "value",\n "key_2": "value_2"\n}'])
@step('I get an error on save$') @step('I get an error on save$')
def error_on_save(step): def error_on_save(step):
assert_regexp_matches(world.css_text('#notification-error-description'), 'Incorrect setting format') assert_regexp_matches(
world.css_text('#notification-error-description'),
"Incorrect format for field '{}'.".format(DISPLAY_NAME_KEY)
)
@step('it is displayed as a string') @step('it is displayed as a string')
...@@ -96,6 +102,20 @@ def the_policy_key_value_is_changed(step): ...@@ -96,6 +102,20 @@ def the_policy_key_value_is_changed(step):
assert_equal(get_display_name_value(), '"foo"') assert_equal(get_display_name_value(), '"foo"')
@step(u'deprecated settings are (then|not) shown$')
def verify_deprecated_settings_shown(_step, expected):
for setting in DEPRECATED_SETTINGS:
if expected == "not":
assert_equal(-1, get_index_of(setting))
else:
world.wait_for(lambda _: get_index_of(setting) != -1)
@step(u'I toggle the display of deprecated settings$')
def toggle_deprecated_settings(_step):
world.css_click(".deprecated-settings-label")
def assert_policy_entries(expected_keys, expected_values): def assert_policy_entries(expected_keys, expected_values):
for key, value in zip(expected_keys, expected_values): for key, value in zip(expected_keys, expected_values):
index = get_index_of(key) index = get_index_of(key)
...@@ -121,9 +141,11 @@ def get_display_name_value(): ...@@ -121,9 +141,11 @@ def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY) index = get_index_of(DISPLAY_NAME_KEY)
return get_codemirror_value(index) return get_codemirror_value(index)
def change_display_name_value(step, new_value): def change_display_name_value(step, new_value):
change_value(step, DISPLAY_NAME_KEY, new_value) change_value(step, DISPLAY_NAME_KEY, new_value)
def change_value(step, key, new_value): def change_value(step, key, new_value):
index = get_index_of(key) index = get_index_of(key)
type_in_codemirror(index, new_value) type_in_codemirror(index, new_value)
......
...@@ -5,7 +5,7 @@ import json ...@@ -5,7 +5,7 @@ import json
from lettuce import world, step from lettuce import world, step
from nose.tools import assert_equal, assert_true # pylint: disable=E0611 from nose.tools import assert_equal, assert_true # pylint: disable=E0611
from common import type_in_codemirror, open_new_course from common import type_in_codemirror, open_new_course
from advanced_settings import change_value from advanced_settings import change_value, ADVANCED_MODULES_KEY
from course_import import import_file from course_import import import_file
DISPLAY_NAME = "Display Name" DISPLAY_NAME = "Display Name"
...@@ -29,7 +29,7 @@ def i_created_unit_with_advanced_module(step, advanced_module): ...@@ -29,7 +29,7 @@ def i_created_unit_with_advanced_module(step, advanced_module):
url = world.browser.url url = world.browser.url
step.given("I select the Advanced Settings") step.given("I select the Advanced Settings")
change_value(step, 'advanced_modules', '["{}"]'.format(advanced_module)) change_value(step, ADVANCED_MODULES_KEY, '["{}"]'.format(advanced_module))
world.visit(url) world.visit(url)
world.wait_for_xmodule() world.wait_for_xmodule()
...@@ -232,7 +232,7 @@ def cancel_does_not_save_changes(step): ...@@ -232,7 +232,7 @@ def cancel_does_not_save_changes(step):
def enable_latex_compiler(step): def enable_latex_compiler(step):
url = world.browser.url url = world.browser.url
step.given("I select the Advanced Settings") step.given("I select the Advanced Settings")
change_value(step, 'use_latex_compiler', 'true') change_value(step, 'Enable LaTeX Compiler', 'true')
world.visit(url) world.visit(url)
world.wait_for_xmodule() world.wait_for_xmodule()
......
...@@ -449,12 +449,12 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -449,12 +449,12 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_fetch_initial_fields(self): def test_fetch_initial_fields(self):
test_model = CourseMetadata.fetch(self.course) test_model = CourseMetadata.fetch(self.course)
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value")
test_model = CourseMetadata.fetch(self.fullcourse) test_model = CourseMetadata.fetch(self.fullcourse)
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in') self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field') self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field') self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
self.assertIn('showanswer', test_model, 'showanswer field ') self.assertIn('showanswer', test_model, 'showanswer field ')
self.assertIn('xqa_key', test_model, 'xqa_key field ') self.assertIn('xqa_key', test_model, 'xqa_key field ')
...@@ -463,8 +463,8 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -463,8 +463,8 @@ class CourseMetadataEditingTest(CourseTestCase):
test_model = CourseMetadata.update_from_json( test_model = CourseMetadata.update_from_json(
self.course, self.course,
{ {
"advertised_start": "start A", "advertised_start": {"value": "start A"},
"days_early_for_beta": 2, "days_early_for_beta": {"value": 2},
}, },
user=self.user user=self.user
) )
...@@ -477,79 +477,60 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -477,79 +477,60 @@ class CourseMetadataEditingTest(CourseTestCase):
test_model = CourseMetadata.update_from_json( test_model = CourseMetadata.update_from_json(
fresh, fresh,
{ {
"advertised_start": "start B", "advertised_start": {"value": "start B"},
"display_name": "jolly roger", "display_name": {"value": "jolly roger"},
}, },
user=self.user user=self.user
) )
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value") self.assertEqual(test_model['display_name']['value'], 'jolly roger', "not expected value")
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value") self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value")
def update_check(self, test_model): def update_check(self, test_model):
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value")
self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field') self.assertIn('advertised_start', test_model, 'Missing new advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start A', "advertised_start not expected value") self.assertEqual(test_model['advertised_start']['value'], 'start A', "advertised_start not expected value")
self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field') self.assertIn('days_early_for_beta', test_model, 'Missing days_early_for_beta metadata field')
self.assertEqual(test_model['days_early_for_beta'], 2, "days_early_for_beta not expected value") self.assertEqual(test_model['days_early_for_beta']['value'], 2, "days_early_for_beta not expected value")
def test_delete_key(self):
test_model = CourseMetadata.update_from_json(
self.fullcourse,
{"unsetKeys": ['showanswer', 'xqa_key']},
user=self.user
)
# ensure no harm
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
# check for deletion effectiveness
self.assertEqual('finished', test_model['showanswer'], 'showanswer field still in')
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
def test_http_fetch_initial_fields(self): def test_http_fetch_initial_fields(self):
response = self.client.get_json(self.course_setting_url) response = self.client.get_json(self.course_setting_url)
test_model = json.loads(response.content) test_model = json.loads(response.content)
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value")
response = self.client.get_json(self.fullcourse_setting_url) response = self.client.get_json(self.fullcourse_setting_url)
test_model = json.loads(response.content) test_model = json.loads(response.content)
self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in') self.assertNotIn('graceperiod', test_model, 'blacklisted field leaked in')
self.assertIn('display_name', test_model, 'full missing editable metadata field') self.assertIn('display_name', test_model, 'full missing editable metadata field')
self.assertEqual(test_model['display_name'], 'Robot Super Course', "not expected value") self.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field') self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
self.assertIn('showanswer', test_model, 'showanswer field ') self.assertIn('showanswer', test_model, 'showanswer field ')
self.assertIn('xqa_key', test_model, 'xqa_key field ') self.assertIn('xqa_key', test_model, 'xqa_key field ')
def test_http_update_from_json(self): def test_http_update_from_json(self):
response = self.client.ajax_post(self.course_setting_url, { response = self.client.ajax_post(self.course_setting_url, {
"advertised_start": "start A", "advertised_start": {"value": "start A"},
"testcenter_info": {"c": "test"}, "days_early_for_beta": {"value": 2},
"days_early_for_beta": 2,
"unsetKeys": ['showanswer', 'xqa_key'],
}) })
test_model = json.loads(response.content) test_model = json.loads(response.content)
self.update_check(test_model) self.update_check(test_model)
self.assertEqual('finished', test_model['showanswer'], 'showanswer field still in')
self.assertEqual(None, test_model['xqa_key'], 'xqa_key field still in')
response = self.client.get_json(self.course_setting_url) response = self.client.get_json(self.course_setting_url)
test_model = json.loads(response.content) test_model = json.loads(response.content)
self.update_check(test_model) self.update_check(test_model)
# now change some of the existing metadata # now change some of the existing metadata
response = self.client.ajax_post(self.course_setting_url, { response = self.client.ajax_post(self.course_setting_url, {
"advertised_start": "start B", "advertised_start": {"value": "start B"},
"display_name": "jolly roger" "display_name": {"value": "jolly roger"}
}) })
test_model = json.loads(response.content) test_model = json.loads(response.content)
self.assertIn('display_name', test_model, 'Missing editable metadata field') self.assertIn('display_name', test_model, 'Missing editable metadata field')
self.assertEqual(test_model['display_name'], 'jolly roger', "not expected value") self.assertEqual(test_model['display_name']['value'], 'jolly roger', "not expected value")
self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field') self.assertIn('advertised_start', test_model, 'Missing revised advertised_start metadata field')
self.assertEqual(test_model['advertised_start'], 'start B', "advertised_start not expected value") self.assertEqual(test_model['advertised_start']['value'], 'start B', "advertised_start not expected value")
def test_advanced_components_munge_tabs(self): def test_advanced_components_munge_tabs(self):
""" """
...@@ -558,13 +539,13 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -558,13 +539,13 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), self.course.tabs) self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), self.course.tabs)
self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), self.course.tabs) self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), self.course.tabs)
self.client.ajax_post(self.course_setting_url, { self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: ["combinedopenended"] ADVANCED_COMPONENT_POLICY_KEY: {"value": ["combinedopenended"]}
}) })
course = modulestore().get_course(self.course.id) course = modulestore().get_course(self.course.id)
self.assertIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs) self.assertIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), course.tabs) self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), course.tabs)
self.client.ajax_post(self.course_setting_url, { self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: [] ADVANCED_COMPONENT_POLICY_KEY: {"value": []}
}) })
course = modulestore().get_course(self.course.id) course = modulestore().get_course(self.course.id)
self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs) self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
......
...@@ -6,6 +6,7 @@ import random ...@@ -6,6 +6,7 @@ import random
import string # pylint: disable=W0402 import string # pylint: disable=W0402
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import django.utils
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.conf import settings from django.conf import settings
...@@ -592,14 +593,14 @@ def _config_course_advanced_components(request, course_module): ...@@ -592,14 +593,14 @@ def _config_course_advanced_components(request, course_module):
component_types = tab_component_map.get(tab_type) component_types = tab_component_map.get(tab_type)
found_ac_type = False found_ac_type = False
for ac_type in component_types: for ac_type in component_types:
if ac_type in request.json[ADVANCED_COMPONENT_POLICY_KEY]: if ac_type in request.json[ADVANCED_COMPONENT_POLICY_KEY]["value"]:
# Add tab to the course if needed # Add tab to the course if needed
changed, new_tabs = add_extra_panel_tab(tab_type, course_module) changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
# If a tab has been added to the course, then send the # If a tab has been added to the course, then send the
# metadata along to CourseMetadata.update_from_json # metadata along to CourseMetadata.update_from_json
if changed: if changed:
course_module.tabs = new_tabs course_module.tabs = new_tabs
request.json.update({'tabs': new_tabs}) request.json.update({'tabs': {'value': new_tabs}})
# Indicate that tabs should not be filtered out of # Indicate that tabs should not be filtered out of
# the metadata # the metadata
filter_tabs = False # Set this flag to avoid the tab removal code below. filter_tabs = False # Set this flag to avoid the tab removal code below.
...@@ -611,7 +612,7 @@ def _config_course_advanced_components(request, course_module): ...@@ -611,7 +612,7 @@ def _config_course_advanced_components(request, course_module):
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module) changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed: if changed:
course_module.tabs = new_tabs course_module.tabs = new_tabs
request.json.update({'tabs':new_tabs}) request.json.update({'tabs': {'value': new_tabs}})
# Indicate that tabs should *not* be filtered out of # Indicate that tabs should *not* be filtered out of
# the metadata # the metadata
filter_tabs = False filter_tabs = False
...@@ -631,8 +632,7 @@ def advanced_settings_handler(request, course_key_string): ...@@ -631,8 +632,7 @@ def advanced_settings_handler(request, course_key_string):
json: get the model json: get the model
PUT, POST PUT, POST
json: update the Course's settings. The payload is a json rep of the json: update the Course's settings. The payload is a json rep of the
metadata dicts. The dict can include a "unsetKeys" entry which is a list metadata dicts.
of keys whose values to unset: i.e., revert to default
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user) course_module = _get_course_module(course_key, request.user)
...@@ -647,9 +647,9 @@ def advanced_settings_handler(request, course_key_string): ...@@ -647,9 +647,9 @@ def advanced_settings_handler(request, course_key_string):
if request.method == 'GET': if request.method == 'GET':
return JsonResponse(CourseMetadata.fetch(course_module)) return JsonResponse(CourseMetadata.fetch(course_module))
else: else:
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = _config_course_advanced_components(request, course_module)
try: try:
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = _config_course_advanced_components(request, course_module)
return JsonResponse(CourseMetadata.update_from_json( return JsonResponse(CourseMetadata.update_from_json(
course_module, course_module,
request.json, request.json,
...@@ -658,7 +658,7 @@ def advanced_settings_handler(request, course_key_string): ...@@ -658,7 +658,7 @@ def advanced_settings_handler(request, course_key_string):
)) ))
except (TypeError, ValueError) as err: except (TypeError, ValueError) as err:
return HttpResponseBadRequest( return HttpResponseBadRequest(
"Incorrect setting format. {}".format(err), django.utils.html.escape(err.message),
content_type="text/plain" content_type="text/plain"
) )
......
from xblock.fields import Scope from xblock.fields import Scope
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from django.utils.translation import ugettext as _
class CourseMetadata(object): class CourseMetadata(object):
...@@ -22,6 +23,10 @@ class CourseMetadata(object): ...@@ -22,6 +23,10 @@ class CourseMetadata(object):
'show_timezone', 'show_timezone',
'format', 'format',
'graded', 'graded',
'hide_from_toc',
'pdf_textbooks',
'name', # from xblock
'tags', # from xblock
'video_speed_optimizations', 'video_speed_optimizations',
] ]
...@@ -40,7 +45,12 @@ class CourseMetadata(object): ...@@ -40,7 +45,12 @@ class CourseMetadata(object):
if field.name in cls.FILTERED_LIST: if field.name in cls.FILTERED_LIST:
continue continue
result[field.name] = field.read_json(descriptor) result[field.name] = {
'value': field.read_json(descriptor),
'display_name': field.display_name,
'help': field.help,
'deprecated': field.runtime_options.get('deprecated', False)
}
return result return result
...@@ -51,30 +61,31 @@ class CourseMetadata(object): ...@@ -51,30 +61,31 @@ class CourseMetadata(object):
Ensures none of the fields are in the blacklist. Ensures none of the fields are in the blacklist.
""" """
dirty = False
# Copy the filtered list to avoid permanently changing the class attribute. # Copy the filtered list to avoid permanently changing the class attribute.
filtered_list = list(cls.FILTERED_LIST) filtered_list = list(cls.FILTERED_LIST)
# Don't filter on the tab attribute if filter_tabs is False. # Don't filter on the tab attribute if filter_tabs is False.
if not filter_tabs: if not filter_tabs:
filtered_list.remove("tabs") filtered_list.remove("tabs")
for key, val in jsondict.iteritems(): # 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? # should it be an error if one of the filtered list items is in the payload?
if key in filtered_list: if key in filtered_list:
continue continue
try:
if key == "unsetKeys": val = model['value']
dirty = True if hasattr(descriptor, key) and getattr(descriptor, key) != val:
for unset in val: key_values[key] = descriptor.fields[key].from_json(val)
descriptor.fields[unset].delete_from(descriptor) except (TypeError, ValueError) as err:
raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}".format(
if hasattr(descriptor, key) and getattr(descriptor, key) != val: name=model['display_name'], detailed_message=err.message)))
dirty = True
value = descriptor.fields[key].from_json(val) for key, value in key_values.iteritems():
setattr(descriptor, key, value) setattr(descriptor, key, value)
if dirty: if len(key_values) > 0:
get_modulestore(descriptor.location).update_item(descriptor, user.id if user else None) get_modulestore(descriptor.location).update_item(descriptor, user.id if user else None)
return cls.fetch(descriptor) return cls.fetch(descriptor)
...@@ -3,22 +3,15 @@ define(["backbone"], function(Backbone) { ...@@ -3,22 +3,15 @@ define(["backbone"], function(Backbone) {
var Advanced = Backbone.Model.extend({ var Advanced = Backbone.Model.extend({
defaults: { defaults: {
// the properties are whatever the user types in (in addition to whatever comes originally from the server) // There will be one property per course setting. Each property's value is an object with keys
// 'display_name', 'help', 'value', and 'deprecated. The property keys are the setting names.
// For instance: advanced_modules: {display_name: "Advanced Modules, help:"Beta modules...",
// value: ["word_cloud", "split_module"], deprecated: False}
// Only 'value' is editable.
}, },
validate: function (attrs) { validate: function (attrs) {
// Keys can no longer be edited. We are currently not validating values. // Keys can no longer be edited. We are currently not validating values.
},
save : function (attrs, options) {
// wraps the save call w/ the deletion of the removed keys after we know the saved ones worked
options = options ? _.clone(options) : {};
// add saveSuccess to the success
var success = options.success;
options.success = function(model, resp, options) {
if (success) success(model, resp, options);
};
Backbone.Model.prototype.save.call(this, attrs, options);
} }
}); });
......
...@@ -4,6 +4,7 @@ define(["js/views/validation", "jquery", "underscore", "gettext", "codemirror"], ...@@ -4,6 +4,7 @@ define(["js/views/validation", "jquery", "underscore", "gettext", "codemirror"],
var AdvancedView = ValidatingView.extend({ var AdvancedView = ValidatingView.extend({
error_saving : "error_saving", error_saving : "error_saving",
successful_changes: "successful_changes", successful_changes: "successful_changes",
render_deprecated: false,
// Model class is CMS.Models.Settings.Advanced // Model class is CMS.Models.Settings.Advanced
events : { events : {
...@@ -29,9 +30,11 @@ var AdvancedView = ValidatingView.extend({ ...@@ -29,9 +30,11 @@ var AdvancedView = ValidatingView.extend({
// iterate through model and produce key : value editors for each property in model.get // iterate through model and produce key : value editors for each property in model.get
var self = this; var self = this;
_.each(_.sortBy(_.keys(this.model.attributes), _.identity), _.each(_.sortBy(_.keys(this.model.attributes), function(key) { return self.model.get(key).display_name; }),
function(key) { function(key) {
listEle$.append(self.renderTemplate(key, self.model.get(key))); if (self.render_deprecated || !self.model.get(key).deprecated) {
listEle$.append(self.renderTemplate(key, self.model.get(key)));
}
}); });
var policyValues = listEle$.find('.json'); var policyValues = listEle$.find('.json');
...@@ -91,7 +94,9 @@ var AdvancedView = ValidatingView.extend({ ...@@ -91,7 +94,9 @@ var AdvancedView = ValidatingView.extend({
} }
} }
if (JSONValue !== undefined) { if (JSONValue !== undefined) {
self.model.set(key, JSONValue); var modelVal = self.model.get(key);
modelVal.value = JSONValue;
self.model.set(key, modelVal);
} }
}); });
}, },
...@@ -120,9 +125,10 @@ var AdvancedView = ValidatingView.extend({ ...@@ -120,9 +125,10 @@ var AdvancedView = ValidatingView.extend({
reset: true reset: true
}); });
}, },
renderTemplate: function (key, value) { renderTemplate: function (key, model) {
var newKeyId = _.uniqueId('policy_key_'), var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({ key : key, value : JSON.stringify(value, null, 4), newEle = this.template({ key: key, display_name : model.display_name, help: model.help,
value : JSON.stringify(model.value, null, 4), deprecated: model.deprecated,
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')}); keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});
this.fieldToSelectorMap[key] = newKeyId; this.fieldToSelectorMap[key] = newKeyId;
......
...@@ -748,6 +748,39 @@ ...@@ -748,6 +748,39 @@
// specific fields - advanced settings // specific fields - advanced settings
&.advanced-policies { &.advanced-policies {
.wrapper-options {
margin: (-$baseline/2) 0 ($baseline/2) 0;
text-align: right;
.wrapper-deprecated-setting {
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: .5;
position: relative;
display: inline-block;
border-radius: 3px;
padding: ($baseline/4) ($baseline/2);
background-color: $gray-l5;
color: $gray-d2;
&:hover {
opacity: 1;
}
&.is-set {
opacity: 1;
background-color: $pink-l5;
color: $pink;
}
}
.deprecated-settings-toggle {
position: absolute;
top: 0;
left: 0;
opacity: 0;
}
}
.field-group { .field-group {
margin-bottom: ($baseline*1.5); margin-bottom: ($baseline*1.5);
...@@ -761,6 +794,11 @@ ...@@ -761,6 +794,11 @@
@include clearfix(); @include clearfix();
position: relative; position: relative;
.title {
margin-top: ($baseline/2);
font-weight: 600;
}
.field { .field {
input { input {
...@@ -768,17 +806,7 @@ ...@@ -768,17 +806,7 @@
} }
.tip { .tip {
@include transition(opacity $tmg-f1 ease-in-out 0s); color: $gray-l1;
opacity: 0.0;
position: absolute;
bottom: ($baseline*1.25);
}
input:focus {
& + .tip {
opacity: 1.0;
}
} }
input.error { input.error {
...@@ -811,6 +839,14 @@ ...@@ -811,6 +839,14 @@
margin: 0; margin: 0;
} }
} }
&.is-deprecated {
background-color: $pink-l5;
.status {
color: $pink-l2;
}
}
} }
.message-error { .message-error {
......
<li class="field-group course-advanced-policy-list-item"> <li class="field-group course-advanced-policy-list-item <%= deprecated ? 'is-deprecated' : '' %>">
<div class="field is-not-editable text key" id="<%= key %>"> <div class="field is-not-editable text key" id="<%= key %>">
<label for="<%= keyUniqueId %>">Policy Key:</label> <h3 class="title" id="<%= keyUniqueId %>"><%= display_name %></h3>
<input readonly title="This field is disabled: policy keys cannot be edited." type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
</div> </div>
<div class="field text value"> <div class="field text value">
<label for="<%= valueUniqueId %>">Policy Value:</label> <label class="sr" for="<%= valueUniqueId %>"><%= display_name %></label>
<textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea> <textarea class="json text" id="<%= valueUniqueId %>"><%= value %></textarea>
<span class="tip tip-stacked"><%= help %></span>
</div> </div>
<% if (deprecated) { %>
<span class="status"><%= gettext("Deprecated") %></span>
<% } %>
</li> </li>
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
% endfor % endfor
<script type="text/javascript"> <script type="text/javascript">
require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/settings/advanced"], require(["domReady!", "jquery", "gettext", "js/models/settings/advanced", "js/views/settings/advanced"],
function(doc, $, AdvancedSettingsModel, AdvancedSettingsView) { function(doc, $, gettext, AdvancedSettingsModel, AdvancedSettingsView) {
$("form :input").focus(function() { $("form :input").focus(function() {
$("label[for='" + this.id + "']").addClass("is-focused"); $("label[for='" + this.id + "']").addClass("is-focused");
}).blur(function() { }).blur(function() {
...@@ -33,6 +33,25 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting ...@@ -33,6 +33,25 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
model: advancedModel model: advancedModel
}); });
editor.render(); editor.render();
$("#deprecated-settings").click(function() {
var $this = $(this);
var wrapperDeprecatedSetting = $('.wrapper-deprecated-setting');
var deprecatedSettingsLabel = $('.deprecated-settings-label');
if ($this.is(':checked')) {
wrapperDeprecatedSetting.addClass('is-set');
deprecatedSettingsLabel.text(gettext('Hide Deprecated Settings'));
editor.render_deprecated = true;
}
else {
wrapperDeprecatedSetting.removeClass('is-set');
deprecatedSettingsLabel.text(gettext('Show Deprecated Settings'));
editor.render_deprecated = false;
}
editor.render();
});
}); });
</script> </script>
...@@ -64,11 +83,18 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting ...@@ -64,11 +83,18 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
<section class="group-settings advanced-policies"> <section class="group-settings advanced-policies">
<header> <header>
<h2 class="title-2">${_("Manual Policy Definition")}</h2> <h2 class="title-2">${_("Manual Policy Definition")}</h2>
</header> </header>
<p class="instructions">${_("<strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.")}</p> <p class="instructions">${_("<strong>Warning</strong>: Do not modify these policies unless you are familiar with their purpose.")}</p>
<div class="wrapper-options">
<div class="wrapper-deprecated-setting">
<input id="deprecated-settings" class="deprecated-settings-toggle" type="checkbox" name="Show Deprecated">
<label for="deprecated-settings" class="deprecated-settings-label">${_("Show Deprecated Settings")}</label>
</div>
</div>
<ul class="list-input course-advanced-policy-list enum"> <ul class="list-input course-advanced-policy-list enum">
</ul> </ul>
......
...@@ -160,7 +160,11 @@ class TextbookList(List): ...@@ -160,7 +160,11 @@ class TextbookList(List):
class CourseFields(object): class CourseFields(object):
lti_passports = List(help="LTI tools passports as id:client_key:client_secret", scope=Scope.settings) lti_passports = List(
display_name=_("LTI Passports"),
help=_("Enter the passports for course LTI tools in the following format: \"id\":\"client_key:client_secret\"."),
scope=Scope.settings
)
textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course", textbooks = TextbookList(help="List of pairs of (title, url) for textbooks used in this course",
default=[], scope=Scope.content) default=[], scope=Scope.content)
...@@ -171,7 +175,11 @@ class CourseFields(object): ...@@ -171,7 +175,11 @@ class CourseFields(object):
default=datetime(2030, 1, 1, tzinfo=UTC()), default=datetime(2030, 1, 1, tzinfo=UTC()),
scope=Scope.settings) scope=Scope.settings)
end = Date(help="Date that this class ends", scope=Scope.settings) end = Date(help="Date that this class ends", scope=Scope.settings)
advertised_start = String(help="Date that this course is advertised to start", scope=Scope.settings) advertised_start = String(
display_name=_("Course Advertised Start Date"),
help=_("Enter the date you want to advertise as the course start date, if this date is different from the set start date. To advertise the set start date, enter null."),
scope=Scope.settings
)
grading_policy = Dict(help="Grading policy definition for this class", grading_policy = Dict(help="Grading policy definition for this class",
default={"GRADER": [ default={"GRADER": [
{ {
...@@ -206,26 +214,108 @@ class CourseFields(object): ...@@ -206,26 +214,108 @@ class CourseFields(object):
"Pass": 0.5 "Pass": 0.5
}}, }},
scope=Scope.content) scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings) show_calculator = Boolean(
display_name = String(help="Display name for this module", default="Empty", display_name=_("Display Name"), scope=Scope.settings) display_name=_("Show Calculator"),
course_edit_method = String(help="Method with which this course is edited.", default="Studio", scope=Scope.settings) help=_("Enter true or false. When true, students can see the calculator in the course."),
show_chat = Boolean(help="Whether to show the chat widget in this course", default=False, scope=Scope.settings) default=False,
scope=Scope.settings
)
display_name = String(
help=_("Enter the name of the course as it should appear in the edX.org course list."),
default="Empty",
display_name=_("Course Display Name"),
scope=Scope.settings
)
course_edit_method = String(
display_name=_("Course Editor"),
help=_("Enter the method by which this course is edited (\"XML\" or \"Studio\")."),
default="Studio",
scope=Scope.settings,
deprecated=True # Deprecated because someone would not edit this value within Studio.
)
show_chat = Boolean(
display_name=_("Show Chat Widget"),
help=_("Enter true or false. When true, students can see the chat widget in the course."),
default=False,
scope=Scope.settings
)
tabs = CourseTabList(help="List of tabs to enable in this course", scope=Scope.settings, default=[]) tabs = CourseTabList(help="List of tabs to enable in this course", scope=Scope.settings, default=[])
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings) end_of_course_survey_url = String(
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings) display_name=_("Course Survey URL"),
discussion_topics = Dict(help="Map of topics names to ids", scope=Scope.settings) help=_("Enter the URL for the end-of-course survey. If your course does not have a survey, enter null."),
discussion_sort_alpha = Boolean(scope=Scope.settings, default=False, help="Sort forum categories and subcategories alphabetically.") scope=Scope.settings
announcement = Date(help="Date this course is announced", scope=Scope.settings) )
cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings) discussion_blackouts = List(
is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings) display_name="Discussion Blackout Dates",
no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings) help=_("Enter pairs of dates between which students cannot post to discussion forums, formatted as \"YYYY-MM-DD-YYYY-MM-DD\". To specify times as well as dates, format the pairs as \"YYYY-MM-DDTHH:MM-YYYY-MM-DDTHH:MM\" (be sure to include the \"T\" between the date and time)."),
disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, scope=Scope.settings) scope=Scope.settings
pdf_textbooks = List(help="List of dictionaries containing pdf_textbook configuration", scope=Scope.settings) )
html_textbooks = List(help="List of dictionaries containing html_textbook configuration", scope=Scope.settings) discussion_topics = Dict(
remote_gradebook = Dict(scope=Scope.settings) display_name=_("Discussion Topic Mapping"),
allow_anonymous = Boolean(scope=Scope.settings, default=True) help=_("Enter discussion categories in the following format: \"CategoryName\": {\"id\": \"i4x-InstitutionName-CourseNumber-course-CourseRun\"}. For example, one discussion category may be \"Lydian Mode\": {\"id\": \"i4x-UniversityX-MUS101-course-2014_T1\"}."),
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False) scope=Scope.settings
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings) )
discussion_sort_alpha = Boolean(
display_name=_("Discussion Sorting Alphabetical"),
scope=Scope.settings, default=False,
help=_("Enter true or false. If true, discussion categories and subcategories are sorted alphabetically. If false, they are sorted chronologically.")
)
announcement = Date(
display_name=_("Course Announcement Date"),
help=_("Enter the date to announce your course."),
scope=Scope.settings
)
cohort_config = Dict(
display_name=_("Cohort Configuration"),
help=_("Cohorts are not currently supported by edX."),
scope=Scope.settings
)
is_new = Boolean(
display_name=_("Course Is New"),
help=_("Enter true or false. If true, the course appears in the list of new courses on edx.org, and a New! badge temporarily appears next to the course image."),
scope=Scope.settings
)
no_grade = Boolean(
display_name=_("Course Not Graded"),
help=_("Enter true or false. If true, the course will not be graded."),
default=False,
scope=Scope.settings
)
disable_progress_graph = Boolean(
display_name=_("Disable Progress Graph"),
help=_("Enter true or false. If true, students cannot view the progress graph."),
default=False,
scope=Scope.settings
)
pdf_textbooks = List(
display_name=_("PDF Textbooks"),
help=_("List of dictionaries containing pdf_textbook configuration"), scope=Scope.settings
)
html_textbooks = List(
display_name=_("HTML Textbooks"),
help=_("For HTML textbooks that appear as separate tabs in the courseware, enter the name of the tab (usually the name of the book) as well as the URLs and titles of all the chapters in the book."),
scope=Scope.settings
)
remote_gradebook = Dict(
display_name=_("Remote Gradebook"),
help=_("Enter the remote gradebook mapping. Only use this setting when REMOTE_GRADEBOOK_URL has been specified."),
scope=Scope.settings
)
allow_anonymous = Boolean(
display_name=_("Allow Anonymous Discussion Posts"),
help=_("Enter true or false. If true, students can create discussion posts that are anonymous to all users."),
scope=Scope.settings, default=True
)
allow_anonymous_to_peers = Boolean(
display_name=_("Allow Anonymous Discussion Posts to Peers"),
help=_("Enter true or false. If true, students can create discussion posts that are anonymous to other students. This setting does not make posts anonymous to course staff."),
scope=Scope.settings, default=False
)
advanced_modules = List(
display_name=_("Advanced Module List"),
help=_("Enter the names of the advanced components to use in your course."),
scope=Scope.settings
)
has_children = True has_children = True
checklists = List(scope=Scope.settings, checklists = List(scope=Scope.settings,
default=[ default=[
...@@ -342,22 +432,33 @@ class CourseFields(object): ...@@ -342,22 +432,33 @@ class CourseFields(object):
"action_text": _("Edit Course Schedule &amp; Details"), "action_text": _("Edit Course Schedule &amp; Details"),
"action_external": False}]} "action_external": False}]}
]) ])
info_sidebar_name = String(scope=Scope.settings, default='Course Handouts') info_sidebar_name = String(
display_name=_("Course Info Sidebar Name"),
help=_("Enter the heading that you want students to see above your course handouts on the Course Info page. Your course handouts appear in the right panel of the page."),
scope=Scope.settings, default='Course Handouts')
show_timezone = Boolean( show_timezone = Boolean(
help="True if timezones should be shown on dates in the courseware. Deprecated in favor of due_date_display_format.", help="True if timezones should be shown on dates in the courseware. Deprecated in favor of due_date_display_format.",
scope=Scope.settings, default=True scope=Scope.settings, default=True
) )
due_date_display_format = String( due_date_display_format = String(
help="Format supported by strftime for displaying due dates. Takes precedence over show_timezone.", display_name=_("Due Date Display Format"),
help=_("Enter the format due dates are displayed in. Due dates must be in MM-DD-YYYY, DD-MM-YYYY, YYYY-MM-DD, or YYYY-DD-MM format."),
scope=Scope.settings, default=None scope=Scope.settings, default=None
) )
enrollment_domain = String(help="External login method associated with user accounts allowed to register in course", enrollment_domain = String(
scope=Scope.settings) display_name=_("External Login Domain"),
certificates_show_before_end = Boolean(help="True if students may download certificates before course end", help=_("Enter the external login method students can use for the course."),
scope=Scope.settings, scope=Scope.settings
default=False) )
certificates_show_before_end = Boolean(
display_name=_("Certificates Downloadable Before End"),
help=_("Enter true or false. If true, students can download certificates before the course ends, if they've met certificate requirements."),
scope=Scope.settings,
default=False
)
course_image = String( course_image = String(
help="Filename of the course image", display_name=_("Course About Page Image"),
help=_("Edit the name of the course image file. You must upload this file on the Files & Uploads page. You can also set the course image on the Settings & Details page."),
scope=Scope.settings, scope=Scope.settings,
# Ensure that courses imported from XML keep their image # Ensure that courses imported from XML keep their image
default="images_course_image.jpg" default="images_course_image.jpg"
...@@ -365,12 +466,14 @@ class CourseFields(object): ...@@ -365,12 +466,14 @@ class CourseFields(object):
## Course level Certificate Name overrides. ## Course level Certificate Name overrides.
cert_name_short = String( cert_name_short = String(
help="Sitewide name of completion statements given to students (short).", help=_("Between quotation marks, enter the short name of the course to use on the certificate that students receive when they complete the course."),
display_name=_("Certificate Name (Short)"),
scope=Scope.settings, scope=Scope.settings,
default="" default=""
) )
cert_name_long = String( cert_name_long = String(
help="Sitewide name of completion statements given to students (long).", help=_("Between quotation marks, enter the long name of the course to use on the certificate that students receive when they complete the course."),
display_name=_("Certificate Name (Long)"),
scope=Scope.settings, scope=Scope.settings,
default="" default=""
) )
...@@ -384,30 +487,55 @@ class CourseFields(object): ...@@ -384,30 +487,55 @@ class CourseFields(object):
# way to add in course-specific styling. There needs to be a discussion # way to add in course-specific styling. There needs to be a discussion
# about the right way to do this, but arjun will address this ASAP. Also # about the right way to do this, but arjun will address this ASAP. Also
# note that the courseware template needs to change when this is removed. # note that the courseware template needs to change when this is removed.
css_class = String(help="DO NOT USE THIS", scope=Scope.settings, default="") css_class = String(
display_name=_("CSS Class for Course Reruns"),
help=_("Allows courses to share the same css class across runs even if they have different numbers."),
scope=Scope.settings, default="",
deprecated=True
)
# TODO: This is a quick kludge to allow CS50 (and other courses) to # TODO: This is a quick kludge to allow CS50 (and other courses) to
# specify their own discussion forums as external links by specifying a # specify their own discussion forums as external links by specifying a
# "discussion_link" in their policy JSON file. This should later get # "discussion_link" in their policy JSON file. This should later get
# folded in with Syllabus, Course Info, and additional Custom tabs in a # folded in with Syllabus, Course Info, and additional Custom tabs in a
# more sensible framework later. # more sensible framework later.
discussion_link = String(help="DO NOT USE THIS", scope=Scope.settings) discussion_link = String(
display_name=_("Discussion Forum External Link"),
help=_("Allows specification of an external link to replace discussion forums."),
scope=Scope.settings,
deprecated=True
)
# TODO: same as above, intended to let internal CS50 hide the progress tab # TODO: same as above, intended to let internal CS50 hide the progress tab
# until we get grade integration set up. # until we get grade integration set up.
# Explicit comparison to True because we always want to return a bool. # Explicit comparison to True because we always want to return a bool.
hide_progress_tab = Boolean(help="DO NOT USE THIS", scope=Scope.settings) hide_progress_tab = Boolean(
display_name=_("Hide Progress Tab"),
help=_("Allows hiding of the progress tab."),
scope=Scope.settings,
deprecated=True
)
display_organization = String(help="An optional display string for the course organization that will get rendered in the LMS", display_organization = String(
scope=Scope.settings) display_name=_("Course Organization Display String"),
help=_("Enter the course organization that you want to appear in the courseware. This setting overrides the organization that you entered when you created the course. To use the organization that you entered when you created the course, enter null."),
scope=Scope.settings
)
display_coursenumber = String(help="An optional display string for the course number that will get rendered in the LMS", display_coursenumber = String(
scope=Scope.settings) display_name=_("Course Number Display String"),
help=_("Enter the course number that you want to appear in the courseware. This setting overrides the course number that you entered when you created the course. To use the course number that you entered when you created the course, enter null."),
scope=Scope.settings
)
max_student_enrollments_allowed = Integer(help="Limit the number of students allowed to enroll in this course.", max_student_enrollments_allowed = Integer(
scope=Scope.settings) display_name=_("Course Maximum Student Enrollment"),
help=_("Enter the maximum number of students that can enroll in the course. To allow an unlimited number of students, enter null."),
scope=Scope.settings
)
allow_public_wiki_access = Boolean(help="Whether to allow an unenrolled user to view the Wiki", allow_public_wiki_access = Boolean(display_name=_("Allow Public Wiki Access"),
help=_("Enter true or false. If true, edX users can view the course wiki even if they're not enrolled in the course."),
default=False, default=False,
scope=Scope.settings) scope=Scope.settings)
......
...@@ -11,6 +11,9 @@ from xblock.runtime import KeyValueStore, KvsFieldData ...@@ -11,6 +11,9 @@ from xblock.runtime import KeyValueStore, KvsFieldData
from xmodule.fields import Date, Timedelta from xmodule.fields import Date, Timedelta
# Make '_' a no-op so we can scrape strings
_ = lambda text: text
class UserPartitionList(List): class UserPartitionList(List):
"""Special List class for listing UserPartitions""" """Special List class for listing UserPartitions"""
...@@ -36,7 +39,8 @@ class InheritanceMixin(XBlockMixin): ...@@ -36,7 +39,8 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.settings scope=Scope.settings
) )
due = Date( due = Date(
help="Date that this problem is due by", display_name=_("Due Date"),
help=_("Enter the default date by which problems are due."),
scope=Scope.settings, scope=Scope.settings,
) )
extended_due = Date( extended_due = Date(
...@@ -48,68 +52,93 @@ class InheritanceMixin(XBlockMixin): ...@@ -48,68 +52,93 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.user_state, scope=Scope.user_state,
) )
course_edit_method = String( course_edit_method = String(
help="Method with which this course is edited.", display_name=_("Course Editor"),
default="Studio", scope=Scope.settings help=_("Enter the method by which this course is edited (\"XML\" or \"Studio\")."),
default="Studio",
scope=Scope.settings,
deprecated=True # Deprecated because user would not change away from Studio within Studio.
) )
giturl = String( giturl = String(
help="url root for course data git repository", display_name=_("GIT URL"),
help=_("Enter the URL for the course data GIT repository."),
scope=Scope.settings,
deprecated=True # Deprecated because GIT workflow users do not use Studio.
)
xqa_key = String(
display_name=_("XQA Key"),
help=_("This setting is not currently supported."), scope=Scope.settings,
deprecated=True
)
annotation_storage_url = String(
help=_("Enter the secret string for annotation storage. The textannotation, videoannotation, and imageannotation advanced modules require this string."),
scope=Scope.settings,
default="http://your_annotation_storage.com",
display_name=_("URL for Annotation Storage")
)
annotation_token_secret = String(
help=_("Enter the location of the annotation storage server. The textannotation, videoannotation, and imageannotation advanced modules require this setting."),
scope=Scope.settings, scope=Scope.settings,
default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
display_name=_("Secret Token String for Annotation")
) )
xqa_key = String(help="DO NOT USE", scope=Scope.settings)
annotation_storage_url = String(help="Location of Annotation backend", scope=Scope.settings, default="http://your_annotation_storage.com", display_name="Url for Annotation Storage")
annotation_token_secret = String(help="Secret string for annotation storage", scope=Scope.settings, default="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", display_name="Secret Token String for Annotation")
graceperiod = Timedelta( graceperiod = Timedelta(
help="Amount of time after the due date that submissions will be accepted", help="Amount of time after the due date that submissions will be accepted",
scope=Scope.settings, scope=Scope.settings,
) )
showanswer = String( showanswer = String(
help="When to show the problem answer to the student", display_name=_("Show Answer"),
help=_("Specify when the Show Answer button appears for each problem. Valid values are \"always\", \"answered\", \"attempted\", \"closed\", \"finished\", \"past_due\", and \"never\"."),
scope=Scope.settings, scope=Scope.settings,
default="finished", default="finished",
) )
rerandomize = String( rerandomize = String(
help="When to rerandomize the problem", display_name=_("Randomization"),
help=_("Specify how often variable values in a problem are randomized when a student loads the problem. Valid values are \"always\", \"onreset\", \"never\", and \"per_student\". This setting only applies to problems that have randomly generated numeric values."),
scope=Scope.settings, scope=Scope.settings,
default="never", default="never",
) )
days_early_for_beta = Float( days_early_for_beta = Float(
help="Number of days early to show content to beta users", display_name=_("Days Early for Beta Users"),
help=_("Enter the number of days before the start date that beta users can access the course."),
scope=Scope.settings, scope=Scope.settings,
default=None, default=None,
) )
static_asset_path = String( static_asset_path = String(
help="Path to use for static assets - overrides Studio c4x://", display_name=_("Static Asset Path"),
help=_("Enter the path to use for files on the Files & Uploads page. This value overrides the Studio default, c4x://."),
scope=Scope.settings, scope=Scope.settings,
default='', default='',
) )
text_customization = Dict( text_customization = Dict(
help="String customization substitutions for particular locations", display_name=_("Text Customization"),
help=_("Enter string customization substitutions for particular locations."),
scope=Scope.settings, scope=Scope.settings,
) )
use_latex_compiler = Boolean( use_latex_compiler = Boolean(
help="Enable LaTeX templates?", display_name=_("Enable LaTeX Compiler"),
help=_("Enter true or false. If true, you can use the LaTeX templates for HTML components and advanced Problem components."),
default=False, default=False,
scope=Scope.settings scope=Scope.settings
) )
max_attempts = Integer( max_attempts = Integer(
display_name="Maximum Attempts", display_name=_("Maximum Attempts"),
help=("Defines the number of times a student can try to answer this problem. " help=_("Enter the maximum number of times a student can try to answer problems. This is a course-wide setting, but you can specify a different number when you create an individual problem. To allow unlimited attempts, enter null."),
"If the value is not set, infinite attempts are allowed."),
values={"min": 0}, scope=Scope.settings values={"min": 0}, scope=Scope.settings
) )
matlab_api_key = String( matlab_api_key = String(
display_name="Matlab API key", display_name=_("Matlab API key"),
help="Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. " help=_("Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. "
"This key is granted for exclusive use by this course for the specified duration. " "This key is granted for exclusive use in this course for the specified duration. "
"Please do not share the API key with other courses and notify MathWorks immediately " "Do not share the API key with other courses. Notify MathWorks immediately "
"if you believe the key is exposed or compromised. To obtain a key for your course, " "if you believe the key is exposed or compromised. To obtain a key for your course, "
"or to report and issue, please contact moocsupport@mathworks.com", "or to report an issue, please contact moocsupport@mathworks.com"),
scope=Scope.settings scope=Scope.settings
) )
# This is should be scoped to content, but since it's defined in the policy # This is should be scoped to content, but since it's defined in the policy
# file, it is currently scoped to settings. # file, it is currently scoped to settings.
user_partitions = UserPartitionList( user_partitions = UserPartitionList(
help="The list of group configurations for partitioning students in content experiments.", display_name=_("Experiment Group Configurations"),
help=_("Enter the configurations that govern how students are grouped for content experiments."),
default=[], default=[],
scope=Scope.settings scope=Scope.settings
) )
......
...@@ -20,6 +20,9 @@ log = logging.getLogger(__name__) ...@@ -20,6 +20,9 @@ log = logging.getLogger(__name__)
# OBSOLETE: This obsoletes 'type' # OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem'] class_priority = ['video', 'problem']
# Make '_' a no-op so we can scrape strings
_ = lambda text: text
class SequenceFields(object): class SequenceFields(object):
has_children = True has_children = True
...@@ -27,7 +30,11 @@ class SequenceFields(object): ...@@ -27,7 +30,11 @@ class SequenceFields(object):
# NOTE: Position is 1-indexed. This is silly, but there are now student # NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix. # positions saved on prod, so it's not easy to fix.
position = Integer(help="Last tab viewed in this sequence", scope=Scope.user_state) position = Integer(help="Last tab viewed in this sequence", scope=Scope.user_state)
due = Date(help="Date that this problem is due by", scope=Scope.settings) due = Date(
display_name=_("Due Date"),
help=_("Enter the date by which problems are due."),
scope=Scope.settings,
)
extended_due = Date( extended_due = Date(
help="Date that this problem is due by for a particular student. This " help="Date that this problem is due by for a particular student. This "
"can be set by an instructor, and will override the global due " "can be set by an instructor, and will override the global due "
......
...@@ -44,11 +44,7 @@ class TestFields(object): ...@@ -44,11 +44,7 @@ class TestFields(object):
values=[{'display_name': 'first', 'value': 'value a'}, values=[{'display_name': 'first', 'value': 'value a'},
{'display_name': 'second', 'value': 'value b'}] {'display_name': 'second', 'value': 'value b'}]
) )
showanswer = String( showanswer = InheritanceMixin.showanswer
help="When to show the problem answer to the student",
scope=Scope.settings,
default="finished"
)
# Used for testing select type # Used for testing select type
float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98]) float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98])
# Used for testing float type # Used for testing float type
......
...@@ -29,11 +29,11 @@ class SplitTest(ContainerBase): ...@@ -29,11 +29,11 @@ class SplitTest(ContainerBase):
course_fix.add_advanced_settings( course_fix.add_advanced_settings(
{ {
u"advanced_modules": ["split_test"], u"advanced_modules": {"value": ["split_test"]},
u"user_partitions": [ u"user_partitions": {"value": [
UserPartition(0, 'Configuration alpha,beta', 'first', [Group("0", 'alpha'), Group("1", 'beta')]).to_json(), UserPartition(0, 'Configuration alpha,beta', 'first', [Group("0", 'alpha'), Group("1", 'beta')]).to_json(),
UserPartition(1, 'Configuration 0,1,2', 'second', [Group("0", 'Group 0'), Group("1", 'Group 1'), Group("2", 'Group 2')]).to_json() UserPartition(1, 'Configuration 0,1,2', 'second', [Group("0", 'Group 0'), Group("1", 'Group 1'), Group("2", 'Group 2')]).to_json()
] ]}
} }
) )
...@@ -100,10 +100,10 @@ class SplitTest(ContainerBase): ...@@ -100,10 +100,10 @@ class SplitTest(ContainerBase):
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta') component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
self.course_fix.add_advanced_settings( self.course_fix.add_advanced_settings(
{ {
u"user_partitions": [ u"user_partitions": {"value": [
UserPartition(0, 'Configuration alpha,beta', 'first', UserPartition(0, 'Configuration alpha,beta', 'first',
[Group("0", 'alpha'), Group("2", 'gamma')]).to_json() [Group("0", 'alpha'), Group("2", 'gamma')]).to_json()
] ]}
} }
) )
self.course_fix._add_advanced_settings() self.course_fix._add_advanced_settings()
......
...@@ -3,6 +3,9 @@ Namespace that defines fields common to all blocks used in the LMS ...@@ -3,6 +3,9 @@ Namespace that defines fields common to all blocks used in the LMS
""" """
from xblock.fields import Boolean, Scope, String, XBlockMixin from xblock.fields import Boolean, Scope, String, XBlockMixin
# Make '_' a no-op so we can scrape strings
_ = lambda text: text
class LmsBlockMixin(XBlockMixin): class LmsBlockMixin(XBlockMixin):
""" """
...@@ -19,19 +22,30 @@ class LmsBlockMixin(XBlockMixin): ...@@ -19,19 +22,30 @@ class LmsBlockMixin(XBlockMixin):
scope=Scope.settings, scope=Scope.settings,
) )
chrome = String( chrome = String(
help="Which chrome to show. Options: \n" display_name=_("Courseware Chrome"),
"chromeless -- No chrome\n" help=_("Enter the chrome, or navigation tools, to use for the XBlock in the LMS. Valid values are: \n"
"tabs -- just tabs\n" "\"chromeless\" -- to not use tabs or the accordion; \n"
"accordion -- just accordion\n" "\"tabs\" -- to use tabs only; \n"
"tabs,accordion -- Full Chrome", "\"accordion\" -- to use the accordion only; or \n"
"\"tabs,accordion\" -- to use tabs and the accordion."),
scope=Scope.settings, scope=Scope.settings,
default=None, default=None,
) )
default_tab = String( default_tab = String(
help="Override which tab is selected. " display_name=_("Default Tab"),
"If not set, courseware tab is shown.", help=_("Enter the tab that is selected in the XBlock. If not set, the Courseware tab is selected."),
scope=Scope.settings, scope=Scope.settings,
default=None, default=None,
) )
source_file = String(help="source file name (eg for latex)", scope=Scope.settings) source_file = String(
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings) display_name=_("LaTeX Source File Name"),
help=_("Enter the source file name for LaTeX."),
scope=Scope.settings,
deprecated=True
)
ispublic = Boolean(
display_name=_("Course Is Public"),
help=_("Enter true or false. If true, the course is open to the public. If false, the course is open only to admins."),
scope=Scope.settings,
deprecated=True
)
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
-e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip -e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@fc5fea25c973ec66d8db63cf69a817ce624f5ef5#egg=XBlock -e git+https://github.com/edx/XBlock.git@aed7a2c51a59836e435259ad0fb41f8e865fa530#egg=XBlock
-e git+https://github.com/edx/codejail.git@71f5c5616e2a73ae8cecd1ff2362774a773d3665#egg=codejail -e git+https://github.com/edx/codejail.git@71f5c5616e2a73ae8cecd1ff2362774a773d3665#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.5.0#egg=diff_cover -e git+https://github.com/edx/diff-cover.git@v0.5.0#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment