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.
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: Support creation and editing of split_test instances (Content Experiments)
......
......@@ -36,7 +36,7 @@ Feature: CMS.Advanced (manual) course policy
@skip_sauce
Scenario: Test how multi-line input appears
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
And I reload the page
Then it is displayed as formatted
......@@ -45,7 +45,7 @@ Feature: CMS.Advanced (manual) course policy
@skip_sauce
Scenario: Test error if value supplied is of the wrong type
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
And I reload the page
Then the policy key value is unchanged
......@@ -67,3 +67,14 @@ Feature: CMS.Advanced (manual) course policy
When I edit the value of a policy key
And I press the "Save" notification button
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
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
KEY_CSS = '.key input.policy-key'
DISPLAY_NAME_KEY = "display_name"
KEY_CSS = '.key h3.title'
DISPLAY_NAME_KEY = "Course Display Name"
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$')
......@@ -58,7 +61,7 @@ def create_value_not_in_quotes(step):
def i_see_default_advanced_settings(step):
# Test only a few of the existing properties (there are around 34 of them)
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$')
......@@ -73,12 +76,15 @@ def they_are_alphabetized(step):
@step('it is displayed as formatted$')
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$')
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')
......@@ -96,6 +102,20 @@ def the_policy_key_value_is_changed(step):
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):
for key, value in zip(expected_keys, expected_values):
index = get_index_of(key)
......@@ -121,9 +141,11 @@ def get_display_name_value():
index = get_index_of(DISPLAY_NAME_KEY)
return get_codemirror_value(index)
def change_display_name_value(step, new_value):
change_value(step, DISPLAY_NAME_KEY, new_value)
def change_value(step, key, new_value):
index = get_index_of(key)
type_in_codemirror(index, new_value)
......
......@@ -5,7 +5,7 @@ import json
from lettuce import world, step
from nose.tools import assert_equal, assert_true # pylint: disable=E0611
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
DISPLAY_NAME = "Display Name"
......@@ -29,7 +29,7 @@ def i_created_unit_with_advanced_module(step, advanced_module):
url = world.browser.url
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.wait_for_xmodule()
......@@ -232,7 +232,7 @@ def cancel_does_not_save_changes(step):
def enable_latex_compiler(step):
url = world.browser.url
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.wait_for_xmodule()
......
......@@ -449,12 +449,12 @@ class CourseMetadataEditingTest(CourseTestCase):
def test_fetch_initial_fields(self):
test_model = CourseMetadata.fetch(self.course)
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)
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.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
self.assertIn('showanswer', test_model, 'showanswer field ')
self.assertIn('xqa_key', test_model, 'xqa_key field ')
......@@ -463,8 +463,8 @@ class CourseMetadataEditingTest(CourseTestCase):
test_model = CourseMetadata.update_from_json(
self.course,
{
"advertised_start": "start A",
"days_early_for_beta": 2,
"advertised_start": {"value": "start A"},
"days_early_for_beta": {"value": 2},
},
user=self.user
)
......@@ -477,79 +477,60 @@ class CourseMetadataEditingTest(CourseTestCase):
test_model = CourseMetadata.update_from_json(
fresh,
{
"advertised_start": "start B",
"display_name": "jolly roger",
"advertised_start": {"value": "start B"},
"display_name": {"value": "jolly roger"},
},
user=self.user
)
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.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):
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.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.assertEqual(test_model['days_early_for_beta'], 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')
self.assertEqual(test_model['days_early_for_beta']['value'], 2, "days_early_for_beta not expected value")
def test_http_fetch_initial_fields(self):
response = self.client.get_json(self.course_setting_url)
test_model = json.loads(response.content)
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)
test_model = json.loads(response.content)
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.assertEqual(test_model['display_name']['value'], 'Robot Super Course', "not expected value")
self.assertIn('rerandomize', test_model, 'Missing rerandomize metadata field')
self.assertIn('showanswer', test_model, 'showanswer field ')
self.assertIn('xqa_key', test_model, 'xqa_key field ')
def test_http_update_from_json(self):
response = self.client.ajax_post(self.course_setting_url, {
"advertised_start": "start A",
"testcenter_info": {"c": "test"},
"days_early_for_beta": 2,
"unsetKeys": ['showanswer', 'xqa_key'],
"advertised_start": {"value": "start A"},
"days_early_for_beta": {"value": 2},
})
test_model = json.loads(response.content)
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)
test_model = json.loads(response.content)
self.update_check(test_model)
# now change some of the existing metadata
response = self.client.ajax_post(self.course_setting_url, {
"advertised_start": "start B",
"display_name": "jolly roger"
"advertised_start": {"value": "start B"},
"display_name": {"value": "jolly roger"}
})
test_model = json.loads(response.content)
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.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):
"""
......@@ -558,13 +539,13 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), self.course.tabs)
self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), self.course.tabs)
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)
self.assertIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
self.assertNotIn(EXTRA_TAB_PANELS.get("notes"), course.tabs)
self.client.ajax_post(self.course_setting_url, {
ADVANCED_COMPONENT_POLICY_KEY: []
ADVANCED_COMPONENT_POLICY_KEY: {"value": []}
})
course = modulestore().get_course(self.course.id)
self.assertNotIn(EXTRA_TAB_PANELS.get("open_ended"), course.tabs)
......
......@@ -6,6 +6,7 @@ import random
import string # pylint: disable=W0402
from django.utils.translation import ugettext as _
import django.utils
from django.contrib.auth.decorators import login_required
from django_future.csrf import ensure_csrf_cookie
from django.conf import settings
......@@ -592,14 +593,14 @@ def _config_course_advanced_components(request, course_module):
component_types = tab_component_map.get(tab_type)
found_ac_type = False
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
changed, new_tabs = add_extra_panel_tab(tab_type, course_module)
# If a tab has been added to the course, then send the
# metadata along to CourseMetadata.update_from_json
if changed:
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
# the metadata
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):
changed, new_tabs = remove_extra_panel_tab(tab_type, course_module)
if changed:
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
# the metadata
filter_tabs = False
......@@ -631,8 +632,7 @@ def advanced_settings_handler(request, course_key_string):
json: get the model
PUT, POST
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
of keys whose values to unset: i.e., revert to default
metadata dicts.
"""
course_key = CourseKey.from_string(course_key_string)
course_module = _get_course_module(course_key, request.user)
......@@ -647,9 +647,9 @@ def advanced_settings_handler(request, course_key_string):
if request.method == 'GET':
return JsonResponse(CourseMetadata.fetch(course_module))
else:
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = _config_course_advanced_components(request, course_module)
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(
course_module,
request.json,
......@@ -658,7 +658,7 @@ def advanced_settings_handler(request, course_key_string):
))
except (TypeError, ValueError) as err:
return HttpResponseBadRequest(
"Incorrect setting format. {}".format(err),
django.utils.html.escape(err.message),
content_type="text/plain"
)
......
from xblock.fields import Scope
from contentstore.utils import get_modulestore
from django.utils.translation import ugettext as _
class CourseMetadata(object):
......@@ -22,6 +23,10 @@ class CourseMetadata(object):
'show_timezone',
'format',
'graded',
'hide_from_toc',
'pdf_textbooks',
'name', # from xblock
'tags', # from xblock
'video_speed_optimizations',
]
......@@ -40,7 +45,12 @@ class CourseMetadata(object):
if field.name in cls.FILTERED_LIST:
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
......@@ -51,30 +61,31 @@ class CourseMetadata(object):
Ensures none of the fields are in the blacklist.
"""
dirty = False
# Copy the filtered list to avoid permanently changing the class attribute.
filtered_list = list(cls.FILTERED_LIST)
# Don't filter on the tab attribute if filter_tabs is False.
if not filter_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?
if key in filtered_list:
continue
if key == "unsetKeys":
dirty = True
for unset in val:
descriptor.fields[unset].delete_from(descriptor)
if hasattr(descriptor, key) and getattr(descriptor, key) != val:
dirty = True
value = descriptor.fields[key].from_json(val)
setattr(descriptor, key, value)
if dirty:
try:
val = model['value']
if hasattr(descriptor, key) and getattr(descriptor, key) != val:
key_values[key] = descriptor.fields[key].from_json(val)
except (TypeError, ValueError) as err:
raise ValueError(_("Incorrect format for field '{name}'. {detailed_message}".format(
name=model['display_name'], detailed_message=err.message)))
for key, value in key_values.iteritems():
setattr(descriptor, key, value)
if len(key_values) > 0:
get_modulestore(descriptor.location).update_item(descriptor, user.id if user else None)
return cls.fetch(descriptor)
......@@ -3,22 +3,15 @@ define(["backbone"], function(Backbone) {
var Advanced = Backbone.Model.extend({
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) {
// 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"],
var AdvancedView = ValidatingView.extend({
error_saving : "error_saving",
successful_changes: "successful_changes",
render_deprecated: false,
// Model class is CMS.Models.Settings.Advanced
events : {
......@@ -29,9 +30,11 @@ var AdvancedView = ValidatingView.extend({
// iterate through model and produce key : value editors for each property in model.get
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) {
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');
......@@ -91,7 +94,9 @@ var AdvancedView = ValidatingView.extend({
}
}
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({
reset: true
});
},
renderTemplate: function (key, value) {
renderTemplate: function (key, model) {
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_')});
this.fieldToSelectorMap[key] = newKeyId;
......
......@@ -748,6 +748,39 @@
// specific fields - advanced settings
&.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 {
margin-bottom: ($baseline*1.5);
......@@ -761,6 +794,11 @@
@include clearfix();
position: relative;
.title {
margin-top: ($baseline/2);
font-weight: 600;
}
.field {
input {
......@@ -768,17 +806,7 @@
}
.tip {
@include transition(opacity $tmg-f1 ease-in-out 0s);
opacity: 0.0;
position: absolute;
bottom: ($baseline*1.25);
}
input:focus {
& + .tip {
opacity: 1.0;
}
color: $gray-l1;
}
input.error {
......@@ -811,6 +839,14 @@
margin: 0;
}
}
&.is-deprecated {
background-color: $pink-l5;
.status {
color: $pink-l2;
}
}
}
.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 %>">
<label for="<%= keyUniqueId %>">Policy Key:</label>
<input readonly title="This field is disabled: policy keys cannot be edited." type="text" class="short policy-key" id="<%= keyUniqueId %>" value="<%= key %>" />
<h3 class="title" id="<%= keyUniqueId %>"><%= display_name %></h3>
</div>
<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>
<span class="tip tip-stacked"><%= help %></span>
</div>
<% if (deprecated) { %>
<span class="status"><%= gettext("Deprecated") %></span>
<% } %>
</li>
......@@ -16,8 +16,8 @@
% endfor
<script type="text/javascript">
require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/settings/advanced"],
function(doc, $, AdvancedSettingsModel, AdvancedSettingsView) {
require(["domReady!", "jquery", "gettext", "js/models/settings/advanced", "js/views/settings/advanced"],
function(doc, $, gettext, AdvancedSettingsModel, AdvancedSettingsView) {
$("form :input").focus(function() {
$("label[for='" + this.id + "']").addClass("is-focused");
}).blur(function() {
......@@ -33,6 +33,25 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
model: advancedModel
});
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>
......@@ -64,11 +83,18 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
<section class="group-settings advanced-policies">
<header>
<h2 class="title-2">${_("Manual Policy Definition")}</h2>
</header>
<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>
......
......@@ -160,7 +160,11 @@ class TextbookList(List):
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",
default=[], scope=Scope.content)
......@@ -171,7 +175,11 @@ class CourseFields(object):
default=datetime(2030, 1, 1, tzinfo=UTC()),
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",
default={"GRADER": [
{
......@@ -206,26 +214,108 @@ class CourseFields(object):
"Pass": 0.5
}},
scope=Scope.content)
show_calculator = Boolean(help="Whether to show the calculator in this course", default=False, scope=Scope.settings)
display_name = String(help="Display name for this module", default="Empty", display_name=_("Display Name"), scope=Scope.settings)
course_edit_method = String(help="Method with which this course is edited.", default="Studio", scope=Scope.settings)
show_chat = Boolean(help="Whether to show the chat widget in this course", default=False, scope=Scope.settings)
show_calculator = Boolean(
display_name=_("Show Calculator"),
help=_("Enter true or false. When true, students can see the calculator in the course."),
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=[])
end_of_course_survey_url = String(help="Url for the end-of-course survey", scope=Scope.settings)
discussion_blackouts = List(help="List of pairs of start/end dates for discussion blackouts", scope=Scope.settings)
discussion_topics = Dict(help="Map of topics names to ids", scope=Scope.settings)
discussion_sort_alpha = Boolean(scope=Scope.settings, default=False, help="Sort forum categories and subcategories alphabetically.")
announcement = Date(help="Date this course is announced", scope=Scope.settings)
cohort_config = Dict(help="Dictionary defining cohort configuration", scope=Scope.settings)
is_new = Boolean(help="Whether this course should be flagged as new", scope=Scope.settings)
no_grade = Boolean(help="True if this course isn't graded", default=False, scope=Scope.settings)
disable_progress_graph = Boolean(help="True if this course shouldn't display the progress graph", default=False, 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)
remote_gradebook = Dict(scope=Scope.settings)
allow_anonymous = Boolean(scope=Scope.settings, default=True)
allow_anonymous_to_peers = Boolean(scope=Scope.settings, default=False)
advanced_modules = List(help="Beta modules used in your course", scope=Scope.settings)
end_of_course_survey_url = String(
display_name=_("Course Survey URL"),
help=_("Enter the URL for the end-of-course survey. If your course does not have a survey, enter null."),
scope=Scope.settings
)
discussion_blackouts = List(
display_name="Discussion Blackout Dates",
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)."),
scope=Scope.settings
)
discussion_topics = Dict(
display_name=_("Discussion Topic Mapping"),
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\"}."),
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
checklists = List(scope=Scope.settings,
default=[
......@@ -342,22 +432,33 @@ class CourseFields(object):
"action_text": _("Edit Course Schedule &amp; Details"),
"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(
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
)
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
)
enrollment_domain = String(help="External login method associated with user accounts allowed to register in course",
scope=Scope.settings)
certificates_show_before_end = Boolean(help="True if students may download certificates before course end",
scope=Scope.settings,
default=False)
enrollment_domain = String(
display_name=_("External Login Domain"),
help=_("Enter the external login method students can use for the course."),
scope=Scope.settings
)
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(
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,
# Ensure that courses imported from XML keep their image
default="images_course_image.jpg"
......@@ -365,12 +466,14 @@ class CourseFields(object):
## Course level Certificate Name overrides.
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,
default=""
)
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,
default=""
)
......@@ -384,30 +487,55 @@ class CourseFields(object):
# 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
# 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
# specify their own discussion forums as external links by specifying a
# "discussion_link" in their policy JSON file. This should later get
# folded in with Syllabus, Course Info, and additional Custom tabs in a
# 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
# until we get grade integration set up.
# 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",
scope=Scope.settings)
display_organization = String(
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",
scope=Scope.settings)
display_coursenumber = String(
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.",
scope=Scope.settings)
max_student_enrollments_allowed = Integer(
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,
scope=Scope.settings)
......
......@@ -11,6 +11,9 @@ from xblock.runtime import KeyValueStore, KvsFieldData
from xmodule.fields import Date, Timedelta
# Make '_' a no-op so we can scrape strings
_ = lambda text: text
class UserPartitionList(List):
"""Special List class for listing UserPartitions"""
......@@ -36,7 +39,8 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.settings
)
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,
)
extended_due = Date(
......@@ -48,68 +52,93 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.user_state,
)
course_edit_method = String(
help="Method with which this course is edited.",
default="Studio", scope=Scope.settings
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 user would not change away from Studio within Studio.
)
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,
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(
help="Amount of time after the due date that submissions will be accepted",
scope=Scope.settings,
)
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,
default="finished",
)
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,
default="never",
)
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,
default=None,
)
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,
default='',
)
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,
)
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,
scope=Scope.settings
)
max_attempts = Integer(
display_name="Maximum Attempts",
help=("Defines the number of times a student can try to answer this problem. "
"If the value is not set, infinite attempts are allowed."),
display_name=_("Maximum Attempts"),
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."),
values={"min": 0}, scope=Scope.settings
)
matlab_api_key = String(
display_name="Matlab API key",
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. "
"Please do not share the API key with other courses and notify MathWorks immediately "
"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",
display_name=_("Matlab API key"),
help=_("Enter the API key provided by MathWorks for accessing the MATLAB Hosted Service. "
"This key is granted for exclusive use in this course for the specified duration. "
"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, "
"or to report an issue, please contact moocsupport@mathworks.com"),
scope=Scope.settings
)
# This is should be scoped to content, but since it's defined in the policy
# file, it is currently scoped to settings.
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=[],
scope=Scope.settings
)
......
......@@ -20,6 +20,9 @@ log = logging.getLogger(__name__)
# OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem']
# Make '_' a no-op so we can scrape strings
_ = lambda text: text
class SequenceFields(object):
has_children = True
......@@ -27,7 +30,11 @@ class SequenceFields(object):
# NOTE: Position is 1-indexed. This is silly, but there are now student
# positions saved on prod, so it's not easy to fix.
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(
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 "
......
......@@ -44,11 +44,7 @@ class TestFields(object):
values=[{'display_name': 'first', 'value': 'value a'},
{'display_name': 'second', 'value': 'value b'}]
)
showanswer = String(
help="When to show the problem answer to the student",
scope=Scope.settings,
default="finished"
)
showanswer = InheritanceMixin.showanswer
# Used for testing select type
float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98])
# Used for testing float type
......
......@@ -29,11 +29,11 @@ class SplitTest(ContainerBase):
course_fix.add_advanced_settings(
{
u"advanced_modules": ["split_test"],
u"user_partitions": [
u"advanced_modules": {"value": ["split_test"]},
u"user_partitions": {"value": [
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()
]
]}
}
)
......@@ -100,10 +100,10 @@ class SplitTest(ContainerBase):
component_editor.set_select_value_and_save('Group Configuration', 'Configuration alpha,beta')
self.course_fix.add_advanced_settings(
{
u"user_partitions": [
u"user_partitions": {"value": [
UserPartition(0, 'Configuration alpha,beta', 'first',
[Group("0", 'alpha'), Group("2", 'gamma')]).to_json()
]
]}
}
)
self.course_fix._add_advanced_settings()
......
......@@ -3,6 +3,9 @@ Namespace that defines fields common to all blocks used in the LMS
"""
from xblock.fields import Boolean, Scope, String, XBlockMixin
# Make '_' a no-op so we can scrape strings
_ = lambda text: text
class LmsBlockMixin(XBlockMixin):
"""
......@@ -19,19 +22,30 @@ class LmsBlockMixin(XBlockMixin):
scope=Scope.settings,
)
chrome = String(
help="Which chrome to show. Options: \n"
"chromeless -- No chrome\n"
"tabs -- just tabs\n"
"accordion -- just accordion\n"
"tabs,accordion -- Full Chrome",
display_name=_("Courseware Chrome"),
help=_("Enter the chrome, or navigation tools, to use for the XBlock in the LMS. Valid values are: \n"
"\"chromeless\" -- to not use tabs or the accordion; \n"
"\"tabs\" -- to use tabs only; \n"
"\"accordion\" -- to use the accordion only; or \n"
"\"tabs,accordion\" -- to use tabs and the accordion."),
scope=Scope.settings,
default=None,
)
default_tab = String(
help="Override which tab is selected. "
"If not set, courseware tab is shown.",
display_name=_("Default Tab"),
help=_("Enter the tab that is selected in the XBlock. If not set, the Courseware tab is selected."),
scope=Scope.settings,
default=None,
)
source_file = String(help="source file name (eg for latex)", scope=Scope.settings)
ispublic = Boolean(help="Whether this course is open to the public, or only to admins", scope=Scope.settings)
source_file = String(
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 @@
-e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip
# 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/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
......
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