Commit 580de552 by Christina Roberts

Merge pull request #4073 from edx/christina/advanced-settings-hack

Display names, help text, and "deprecated" for Advanced Settings.
parents 3c838081 a4b172d9
...@@ -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>
......
...@@ -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