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:
try:
# Whether or not to filter the tabs key out of the settings metadata
filter_tabs = _config_course_advanced_components(request, course_module)
try:
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)
try:
val = model['value']
if hasattr(descriptor, key) and getattr(descriptor, key) != val:
dirty = True
value = descriptor.fields[key].from_json(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 dirty:
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) {
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>
......@@ -69,6 +88,13 @@ require(["domReady!", "jquery", "js/models/settings/advanced", "js/views/setting
<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>
......
......@@ -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 "
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 and issue, please contact moocsupport@mathworks.com",
"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