Commit 4a64c0c9 by Jay Zoldak

Merge in from origin/feature/christina/metadata-ui

parents 6627ca56 5d41e2a9
......@@ -49,8 +49,10 @@ def verify_all_setting_entries(expected_entries):
settings = world.browser.find_by_css('.wrapper-comp-setting')
assert_equal(len(expected_entries), len(settings))
for (counter, setting) in enumerate(settings):
world.verify_setting_entry(setting, expected_entries[counter][0],
expected_entries[counter][1], expected_entries[counter][2])
world.verify_setting_entry(
setting, expected_entries[counter][0],
expected_entries[counter][1], expected_entries[counter][2]
)
@world.absorb
......
......@@ -6,12 +6,15 @@ from lettuce import world, step
@step('I have created a Discussion Tag$')
def i_created_discussion_tag(step):
world.create_component_instance(step, '.large-discussion-icon', 'i4x://edx/templates/discussion/Discussion_Tag',
'.xmodule_DiscussionModule')
world.create_component_instance(
step, '.large-discussion-icon',
'i4x://edx/templates/discussion/Discussion_Tag',
'.xmodule_DiscussionModule'
)
@step('I see three alphabetized settings and their expected values$')
def i_see_only_the_display_name(step):
def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries(
[
['Category', "Week 1", True],
......
......@@ -6,8 +6,10 @@ from lettuce import world, step
@step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step):
world.create_component_instance(step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page',
'.xmodule_HtmlModule')
world.create_component_instance(
step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page',
'.xmodule_HtmlModule'
)
@step('I see only the HTML display name setting$')
......
......@@ -13,6 +13,12 @@ Feature: Problem Editor
Then I can modify the display name
And my display name change is persisted on save
Scenario: User can specify special characters in String values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can specify special characters in the display name
And my special characters and persisted on save
Scenario: User can revert display name to unset
Given I have created a Blank Common Problem
And I edit and select Settings
......@@ -59,4 +65,3 @@ Feature: Problem Editor
Given I have created a LaTeX Problem
And I edit and select Settings
Then Edit High Level Source is visible
......@@ -14,8 +14,17 @@ SHOW_ANSWER = "Show Answer"
############### ACTIONS ####################
@step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step):
<<<<<<< HEAD
world.create_component_instance(step, '.large-problem-icon', 'i4x://edx/templates/problem/Blank_Common_Problem',
'.xmodule_CapaModule')
=======
world.create_component_instance(
step,
'.large-problem-icon',
'i4x://edx/templates/problem/Blank_Common_Problem',
'.xmodule_CapaModule'
)
>>>>>>> a0efd3a5d7045dd9369869fd15fc2cc4ecdb6cc1
@step('I edit and select Settings$')
......@@ -47,6 +56,21 @@ def my_display_name_change_is_persisted_on_save(step):
verify_modified_display_name()
<<<<<<< HEAD
=======
@step('I can specify special characters in the display name')
def i_can_modify_the_display_name_with_special_chars(step):
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill("updated ' \" &")
verify_modified_display_name_with_special_chars()
@step('my special characters and persisted on save')
def special_chars_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_modified_display_name_with_special_chars()
>>>>>>> a0efd3a5d7045dd9369869fd15fc2cc4ecdb6cc1
@step('I can revert the display name to unset')
def can_revert_display_name_to_unset(step):
world.revert_setting_entry(DISPLAY_NAME)
......@@ -75,7 +99,8 @@ def my_change_to_randomization_is_persisted(step):
def i_can_revert_to_default_for_randomization(step):
world.revert_setting_entry(RANDOMIZATION)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
@step('I can set the weight to 3.5')
......@@ -94,33 +119,37 @@ def my_change_to_randomization_is_persisted(step):
def i_can_revert_to_default_for_randomization(step):
world.revert_setting_entry(PROBLEM_WEIGHT)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
@step('if I set the weight to abc, it remains unset')
def set_the_weight_to_abc(step):
world.get_setting_entry(PROBLEM_WEIGHT).find_by_css('.setting-input')[0].fill('abc')
# We show the clear button immediately on type, hence the "True" here.
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", True)
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", True)
world.save_component_and_reopen(step)
# But no change was actually ever sent to the model, so on reopen, explicitly_set is False
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
@step('if I set the max attempts to 2.34, the max attempts are persisted as 234')
def set_the_weight_to_abc(step):
world.get_setting_entry(MAXIMUM_ATTEMPTS).find_by_css('.setting-input')[0].fill('2.34')
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, "234", True)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, "234", True)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, "234", True)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, "234", True)
@step('I set the max attempts to -3, the max attempts are persisted as 1')
def set_max_attempts_to_neg_3(step):
world.get_setting_entry(MAXIMUM_ATTEMPTS).find_by_css('.setting-input')[0].fill('-3')
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, "-3", True)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, "-3", True)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, "1", True)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, "1", True)
@step('Edit High Level Source is not visible')
......@@ -156,16 +185,25 @@ def verify_high_level_source(step, visible):
def verify_modified_weight():
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "3.5", True)
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "3.5", True)
def verify_modified_randomization():
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Per Student", True)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Per Student", True)
def verify_modified_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
<<<<<<< HEAD
=======
def verify_modified_display_name_with_special_chars():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, "updated ' \" &", True)
>>>>>>> a0efd3a5d7045dd9369869fd15fc2cc4ecdb6cc1
def verify_unset_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False)
......@@ -3,10 +3,15 @@
from lettuce import world, step
@step('I have created a Video component$')
def i_created_a_video_component(step):
world.create_component_instance(step, '.large-video-icon', 'i4x://edx/templates/video/default',
'.xmodule_VideoModule')
world.create_component_instance(
step, '.large-video-icon',
'i4x://edx/templates/video/default',
'.xmodule_VideoModule'
)
@step('I see only the video display name setting$')
def i_see_only_the_video_display_name(step):
......
../../../templates/js/metadata-editor.underscore
\ No newline at end of file
../../../templates/js/metadata-number-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-option-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-string-entry.underscore
\ No newline at end of file
......@@ -27,7 +27,7 @@ describe "AJAX Errors", ->
tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
appendSetFixtures(sandbox({id: "page-notification"}))
@requests = requests = []
@xhr = sinon.useFakeXMLHttpRequest()
......
tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures(sandbox({id: "page-alert"}))
appendSetFixtures(sandbox({id: "page-alert"}))
appendSetFixtures(sandbox({id: "page-notification"}))
appendSetFixtures(sandbox({id: "page-prompt"}))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(tpl))
......
......@@ -4,7 +4,7 @@ describe "CMS.Views.ModuleEdit", ->
@stubModule.id = 'stub-id'
setFixtures """
appendSetFixtures """
<li class="component" id="stub-id">
<div class="component-editor">
<div class="module-editor">
......
......@@ -29,7 +29,7 @@ describe "CMS.Views.SectionEdit", ->
feedback_tpl = readFixtures('system-feedback.underscore')
beforeEach ->
setFixtures($("<script>", {id: "section-name-edit-tpl", type: "text/template"}).text(tpl))
appendSetFixtures($("<script>", {id: "section-name-edit-tpl", type: "text/template"}).text(tpl))
appendSetFixtures($("<script>", {id: "system-feedback-tpl", type: "text/template"}).text(feedback_tpl))
spyOn(CMS.Views.SectionEdit.prototype, "switchToShowView")
.andCallThrough()
......
......@@ -126,7 +126,6 @@ class CMS.Views.ModuleEdit extends Backbone.View
hideDataEditor: =>
editorModeButtonParent = @$el.find('#editor-mode')
# Can it be enough to just remove active-mode?
editorModeButtonParent.addClass('inactive-mode')
editorModeButtonParent.removeClass('active-mode')
@$el.find('.wrapper-comp-settings').addClass('is-active')
......
......@@ -825,6 +825,3 @@ function saveSetSectionScheduleDate(e) {
hideModal();
});
}
......@@ -3,7 +3,6 @@ if (!CMS.Views['Metadata']) CMS.Views.Metadata = {};
CMS.Views.Metadata.Editor = Backbone.View.extend({
// Model is simply a Backbone.Model instance.
initialize : function() {
var tpl = $("#metadata-editor-tpl").text();
if(!tpl) {
......@@ -11,7 +10,7 @@ CMS.Views.Metadata.Editor = Backbone.View.extend({
}
this.template = _.template(tpl);
this.$el.html(this.template({metadata_entries: this.model.attributes}));
this.$el.html(this.template({numEntries: this.model.keys().length}));
var counter = 0;
// Sort entries by display name.
......@@ -45,6 +44,9 @@ CMS.Views.Metadata.Editor = Backbone.View.extend({
});
},
/**
* Returns the just the modified metadata values, in the format used to persist to the server.
*/
getModifiedMetadataValues: function () {
var modified_values = {};
_.each(this.models,
......@@ -57,8 +59,16 @@ CMS.Views.Metadata.Editor = Backbone.View.extend({
return modified_values;
},
/**
* Returns a display name for the component related to this metadata. This method looks to see
* if there is a metadata entry called 'display_name', and if so, it returns its value. If there
* is no such entry, or if display_name does not have a value set, it returns an empty string.
*/
getDisplayName: function () {
// It is possible that there is no display name set. In that case, return empty string.
if (this.model.get('display_name') === undefined) {
return '';
}
var displayNameValue = this.model.get('display_name').value;
return displayNameValue ? displayNameValue : '';
}
......@@ -67,13 +77,13 @@ CMS.Views.Metadata.Editor = Backbone.View.extend({
CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
// Model is CMS.Models.Metadata.
initialize : function() {
var self = this;
var templateName = this.getTemplateName();
var templateName = _.result(this, 'templateName');
// Backbone model cid is only unique within the collection.
this.uniqueId = _.uniqueId(templateName + "_");
var tpl = $("#"+templateName).text();
var tpl = document.getElementById(templateName).text;
if(!tpl) {
console.error("Couldn't load template: " + templateName);
}
......@@ -82,22 +92,42 @@ CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
this.render();
},
getTemplateName : function () {},
/**
* The ID/name of the template. Subclasses must override this.
*/
templateName: '',
/**
* Returns the value currently displayed in the editor/view. Subclasses should implement this method.
*/
getValueFromEditor : function () {},
/**
* Sets the value currently displayed in the editor/view. Subclasses should implement this method.
*/
setValueInEditor : function (value) {},
/**
* Sets the value in the model, using the value currently displayed in the view. Afterward,
* this method re-renders to update the clear button.
*/
updateModel: function () {
this.model.setValue(this.getValueFromEditor());
this.render();
},
/**
* Clears the value currently set in the model (reverting to the default). Afterward, this method
* re-renders the view.
*/
clear: function () {
this.model.clear();
this.render();
},
/**
* Shows the clear button, if it is not already showing.
*/
showClearButton: function() {
if (!this.$el.hasClass('is-set')) {
this.$el.addClass('is-set');
......@@ -106,10 +136,17 @@ CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
}
},
/**
* Returns the clear button.
*/
getClearButton: function () {
return this.$el.find('.setting-clear');
},
/**
* Renders the editor, updating the value displayed in the view, as well as the state of
* the clear button.
*/
render: function () {
if (!this.template) return;
......@@ -134,9 +171,7 @@ CMS.Views.Metadata.String = CMS.Views.Metadata.AbstractEditor.extend({
"click .setting-clear" : "clear"
},
getTemplateName : function () {
return "metadata-string-entry";
},
templateName: "metadata-string-entry",
getValueFromEditor : function () {
return this.$el.find('#' + this.uniqueId).val();
......@@ -172,7 +207,7 @@ CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
}
if (options.hasOwnProperty(max)) {
this.max = Number(options[max]);
this.$el.find('input').attr(max, numToString(this.max.toFixed));
this.$el.find('input').attr(max, numToString(this.max));
}
var stepValue = undefined;
if (options.hasOwnProperty(step)) {
......@@ -196,9 +231,7 @@ CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
}
},
getTemplateName : function () {
return "metadata-number-entry";
},
templateName: "metadata-number-entry",
getValueFromEditor : function () {
return this.$el.find('#' + this.uniqueId).val();
......@@ -208,6 +241,9 @@ CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
this.$el.find('input').val(value);
},
/**
* Returns true if this view is restricted to integers, as opposed to floating points values.
*/
isIntegerField : function () {
return this.model.getType() === 'Integer';
},
......@@ -249,9 +285,7 @@ CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
"click .setting-clear" : "clear"
},
getTemplateName : function () {
return "metadata-option-entry";
},
templateName: "metadata-option-entry",
getValueFromEditor : function () {
var selectedText = this.$el.find('#' + this.uniqueId).find(":selected").text();
......@@ -275,7 +309,7 @@ CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
value = modelValue['display_name'];
}
});
$('#' + this.uniqueId + " option").filter(function() {
this.$el.find('#' + this.uniqueId + " option").filter(function() {
return $(this).text() === value;
}).prop('selected', true);
}
......
<ul class="list-input settings-list">
<% _.each(metadata_entries, function(entry) { %>
<% _.each(_.range(numEntries), function() { %>
<li class="field comp-setting-entry metadata_entry" id="settings-listing">
</li>
<% }) %>
......
......@@ -26,7 +26,7 @@
</script>
<% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %>
<% metadata_field_copy = copy.deepcopy(editable_metadata_fields) %>
<% metadata_field_copy = copy.copy(editable_metadata_fields) %>
## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it.
% if 'source_code' in editable_metadata_fields:
## source-edit.html needs access to the 'source_code' value, so delete from a copy.
......@@ -40,4 +40,4 @@
<%include file="source-edit.html" />
% endif
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(metadata_field_copy)}'/>
\ No newline at end of file
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(metadata_field_copy) | h}'/>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-comp-editor" id="editor-tab">
<section class="problem-editor editor">
<div class="row">
%if enable_markdown:
<div class="editor-bar">
<ul class="format-buttons">
<li><a href="#" class="header-button" data-tooltip="Heading 1"><span
<li><a href="#" class="header-button" data-tooltip='${_("Heading 1")}'><span
class="problem-editor-icon heading1"></span></a></li>
<li><a href="#" class="multiple-choice-button" data-tooltip="Multiple Choice"><span
<li><a href="#" class="multiple-choice-button" data-tooltip='${_("Multiple Choice")}'><span
class="problem-editor-icon multiple-choice"></span></a></li>
<li><a href="#" class="checks-button" data-tooltip="Checkboxes"><span
<li><a href="#" class="checks-button" data-tooltip='${_("Checkboxes")}'><span
class="problem-editor-icon checks"></span></a></li>
<li><a href="#" class="string-button" data-tooltip="Text Input"><span
<li><a href="#" class="string-button" data-tooltip='${_("Text Input")}'><span
class="problem-editor-icon string"></span></a></li>
<li><a href="#" class="number-button" data-tooltip="Numerical Input"><span
<li><a href="#" class="number-button" data-tooltip='${_("Numerical Input")}'><span
class="problem-editor-icon number"></span></a></li>
<li><a href="#" class="dropdown-button" data-tooltip="Dropdown"><span
<li><a href="#" class="dropdown-button" data-tooltip='${_("Dropdown")}'><span
class="problem-editor-icon dropdown"></span></a></li>
<li><a href="#" class="explanation-button" data-tooltip="Explanation"><span
<li><a href="#" class="explanation-button" data-tooltip='${_("Explanation")}'><span
class="problem-editor-icon explanation"></span></a></li>
</ul>
<ul class="editor-tabs">
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">Advanced Editor</a></li>
<li><a href="#" class="cheatsheet-toggle" data-tooltip="Toggle Cheatsheet">?</a></li>
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">${_("Advanced Editor")}</a></li>
<li><a href="#" class="cheatsheet-toggle" data-tooltip='${_("Toggle Cheatsheet")}'>?</a></li>
</ul>
</div>
<textarea class="markdown-box">${markdown | h}</textarea>
......@@ -34,7 +36,7 @@
<article class="simple-editor-cheatsheet">
<div class="cheatsheet-wrapper">
<div class="row">
<h6>Heading 1</h6>
<h6>${_("Heading 1")}</h6>
<div class="col sample heading-1">
<img src="/static/img/header-example.png" />
</div>
......@@ -45,7 +47,7 @@
</div>
</div>
<div class="row">
<h6>Multiple Choice</h6>
<h6>${_("Multiple Choice")}</h6>
<div class="col sample multiple-choice">
<img src="/static/img/choice-example.png" />
</div>
......@@ -56,7 +58,7 @@
</div>
</div>
<div class="row">
<h6>Checkboxes</h6>
<h6>${_("Checkboxes")}</h6>
<div class="col sample check-multiple">
<img src="/static/img/multi-example.png" />
</div>
......@@ -67,7 +69,7 @@
</div>
</div>
<div class="row">
<h6>Text Input</h6>
<h6>${_("Text Input")}</h6>
<div class="col sample string-response">
<img src="/static/img/string-example.png" />
</div>
......@@ -76,7 +78,7 @@
</div>
</div>
<div class="row">
<h6>Numerical Input</h6>
<h6>${_("Numerical Input")}</h6>
<div class="col sample numerical-response">
<img src="/static/img/number-example.png" />
</div>
......@@ -85,7 +87,7 @@
</div>
</div>
<div class="row">
<h6>Dropdown</h6>
<h6>${_("Dropdown")}</h6>
<div class="col sample option-reponse">
<img src="/static/img/select-example.png" />
</div>
......@@ -94,7 +96,7 @@
</div>
</div>
<div class="row">
<h6>Explanation</h6>
<h6>${_("Explanation")}</h6>
<div class="col sample explanation">
<img src="/static/img/explanation-example.png" />
</div>
......
......@@ -66,13 +66,16 @@ class ComplexEncoder(json.JSONEncoder):
class CapaFields(object):
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
max_attempts = StringyInteger(display_name="Maximum Attempts",
help="This specifies the number of times the student can try to answer this problem. If unset, infinite attempts are allowed.",
values = {"min" : 1 }, scope=Scope.settings)
max_attempts = StringyInteger(
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.",
values = {"min" : 1 }, scope=Scope.settings
)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
showanswer = String(display_name="Show Answer",
help="Specifies when to show the answer to this problem. A default value can be set course-wide in Advanced Settings.",
showanswer = String(
display_name="Show Answer",
help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.",
scope=Scope.settings, default="closed",
values=[
{"display_name": "Always", "value": "always"},
......@@ -81,26 +84,33 @@ class CapaFields(object):
{"display_name": "Closed", "value": "closed"},
{"display_name": "Finished", "value": "finished"},
{"display_name": "Past Due", "value": "past_due"},
{"display_name": "Never", "value": "never"}])
{"display_name": "Never", "value": "never"}]
)
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
rerandomize = Randomization(display_name="Randomization", help="Specifies whether variable inputs for this problem are randomized each time a student loads the problem. This only applies to problems that have randomly generated numeric variables. A default value can be set course-wide in Advanced Settings.",
rerandomize = Randomization(
display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.",
default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"},
{"display_name": "On Reset", "value": "onreset"},
{"display_name": "Never", "value": "never"},
{"display_name": "Per Student", "value": "per_student"}])
{"display_name": "Per Student", "value": "per_student"}]
)
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
weight = StringyFloat(display_name="Problem Weight",
help="Specifies the number of points the problem is worth. If unset, each response field in the problem is worth one point.",
weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
values = {"min" : 0 , "step": .1},
scope=Scope.settings)
scope=Scope.settings
)
markdown = String(help="Markdown source of this module", scope=Scope.settings)
source_code = String(help="Source code for LaTeX and Word problems. This feature is not well-supported.",
scope=Scope.settings)
source_code = String(
help="Source code for LaTeX and Word problems. This feature is not well-supported.",
scope=Scope.settings
)
class CapaModule(CapaFields, XModule):
......
......@@ -48,33 +48,49 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
display_name = String(
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
default="Open Ended Grading", scope=Scope.settings
)
current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state)
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.user_state)
ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state)
attempts = StringyInteger(display_name="Maximum Attempts",
ready_to_reset = StringyBoolean(
help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state
)
attempts = StringyInteger(
display_name="Maximum Attempts",
help="The number of times the student can try to answer this problem.", default=1,
scope=Scope.settings, values = {"min" : 1 })
scope=Scope.settings, values = {"min" : 1 }
)
is_graded = StringyBoolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = StringyBoolean(display_name="Allow File Uploads",
help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings)
skip_spelling_checks = StringyBoolean(display_name="Disable Quality Filter",
# TODO: passing of text failed with "won't". Need to make our code more robust.
help="If False, submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
default=False, scope=Scope.settings)
accept_file_upload = StringyBoolean(
display_name="Allow File Uploads",
help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings
)
skip_spelling_checks = StringyBoolean(
display_name="Disable Quality Filter",
help="If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
default=False, scope=Scope.settings
)
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
scope=Scope.settings)
graceperiod = String(
help="Amount of time after the due date that submissions will be accepted",
default=None,
scope=Scope.settings
)
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
weight = StringyFloat(display_name="Problem Weight",
help="The number of points the problem is worth. By default, each problem is worth one point.",
scope=Scope.settings, values = {"min" : 0 , "step": ".1"})
weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
)
markdown = String(help="Markdown source of this module", scope=Scope.settings)
......
......@@ -8,12 +8,16 @@ from xblock.core import String, Scope
class DiscussionFields(object):
discussion_id = String(scope=Scope.settings)
discussion_category = String(display_name="Category",
help="Specifies a category name for this discussion. This name appears in the left pane of the discussion forum for your course.",
scope=Scope.settings)
discussion_target = String(display_name="Subcategory",
help="Specifies a subcategory name for this discussion. This name appears in the left pane of the discussion forum for your course.",
scope=Scope.settings)
discussion_category = String(
display_name="Category",
help="A category name for the discussion. This name appears in the left pane of the discussion forum for the course.",
scope=Scope.settings
)
discussion_target = String(
display_name="Subcategory",
help="A subcategory name for the discussion. This name appears in the left pane of the discussion forum for the course.",
scope=Scope.settings
)
sort_key = String(scope=Scope.settings)
......
......@@ -28,25 +28,37 @@ EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please
class PeerGradingFields(object):
use_for_single_location = StringyBoolean(display_name="Show Single Problem",
use_for_single_location = StringyBoolean(
display_name="Show Single Problem",
help='When True, only the single problem specified by "Link to Problem Location" is shown. '
'When False, a panel is displayed with all problems available for peer grading.',
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
link_to_location = String(display_name="Link to Problem Location",
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings
)
link_to_location = String(
display_name="Link to Problem Location",
help='The location of the problem being graded. Only used when "Show Single Problem" is True.',
default=LINK_TO_LOCATION, scope=Scope.settings)
is_graded = StringyBoolean(display_name="Graded",
help='Whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.',
default=IS_GRADED, scope=Scope.settings)
default=LINK_TO_LOCATION, scope=Scope.settings
)
is_graded = StringyBoolean(
display_name="Graded",
help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.',
default=IS_GRADED, scope=Scope.settings
)
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
max_grade = StringyInteger(help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
scope=Scope.settings, values={"min" : 0 })
student_data_for_location = Object(help="Student data for a given peer grading problem.",
scope=Scope.user_state)
weight = StringyFloat(display_name="Problem Weight",
help="Specifies the number of points the problem is worth. By default, each problem is worth one point.",
scope=Scope.settings, values = {"min" : 0 , "step": ".1"})
max_grade = StringyInteger(
help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
scope=Scope.settings, values={"min": 0}
)
student_data_for_location = Object(
help="Student data for a given peer grading problem.",
scope=Scope.user_state
)
weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
scope=Scope.settings, values={"min": 0, "step": ".1"}
)
class PeerGradingModule(PeerGradingFields, XModule):
......
......@@ -34,5 +34,4 @@ data: |
</task>
</combinedopenended>
children: []
......@@ -3,8 +3,6 @@ metadata:
display_name: Multiple Choice
rerandomize: never
showanswer: finished
weight: ""
attempts: ""
markdown:
"A multiple choice problem presents radio buttons for student input. Students can only select a single
option presented. Multiple Choice questions have been the subject of many areas of research due to the early
......
......@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase):
)
module = modulestore.get_instance(course.id, location)
self.assertEqual(len(module.get_children()), 0)
self.assertEqual(module.num_inputs, '5')
self.assertEqual(module.num_top_words, '250')
self.assertEqual(module.num_inputs, 5)
self.assertEqual(module.num_top_words, 250)
def test_cohort_config(self):
"""
......
#pylint: disable=C0111
#pylint: disable=W0621
from xmodule.x_module import XModuleFields
from xblock.core import Scope, String, Object, Boolean
from xmodule.fields import Date, StringyInteger, StringyFloat
from xmodule.xml_module import XmlDescriptor
import unittest
from . import test_system
from .import test_system
from mock import Mock
class CrazyJsonString(String):
def to_json(self, value):
return value + " JSON"
class TestFields(object):
# Will be returned by editable_metadata_fields.
max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1 , 'max' : 10})
max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
due = Date(scope=Scope.settings)
# Will not be returned by editable_metadata_fields because is not Scope.settings.
student_answers = Object(scope=Scope.user_state)
# Will be returned, and can override the inherited value from XModule.
display_name = String(scope=Scope.settings, default='local default', display_name = 'Local Display Name',
help='local help')
display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
help='local help')
# Used for testing select type, effect of to_json method
string_select = CrazyJsonString(scope=Scope.settings, default='default value',
values=[{'display_name' : 'first', 'value' : 'value a'},
{'display_name' : 'second','value' : 'value b'}])
string_select = CrazyJsonString(
scope=Scope.settings,
default='default value',
values=[{'display_name': 'first', 'value': 'value a'},
{'display_name': 'second', 'value': 'value b'}]
)
# Used for testing select type
float_select = StringyFloat(scope=Scope.settings, default=.999, values=[1.23, 0.98])
# Used for testing float type
float_non_select = StringyFloat(scope=Scope.settings, default=.999, values={'min': 0 , 'step' : .3})
float_non_select = StringyFloat(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
# Used for testing that Booleans get mapped to select type
boolean_select = Boolean(scope=Scope.settings)
class EditableMetadataFieldsTest(unittest.TestCase):
def test_display_name_field(self):
editable_fields = self.get_xml_editable_fields({})
# Tests that the xblock fields (currently tags and name) get filtered out.
# Also tests that xml_attributes is filtered out of XmlDescriptor.
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.")
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=False, inheritable=False, value=None, default_value=None)
self.assert_field_values(
editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=False, inheritable=False, value=None, default_value=None
)
def test_override_default(self):
# Tests that explicitly_set is correct when a value overrides the default (not inheritable).
editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=True, inheritable=False, value='foo', default_value=None)
self.assert_field_values(
editable_fields, 'display_name', XModuleFields.display_name,
explicitly_set=True, inheritable=False, value='foo', default_value=None
)
def test_integer_field(self):
descriptor = self.get_descriptor({'max_attempts' : '7'})
descriptor = self.get_descriptor({'max_attempts': '7'})
editable_fields = descriptor.editable_metadata_fields
self.assertEqual(6, len(editable_fields))
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
self.assert_field_values(
editable_fields, 'max_attempts', TestFields.max_attempts,
explicitly_set=True, inheritable=False, value=7, default_value=1000, type='Integer',
options=TestFields.max_attempts.values)
self.assert_field_values(editable_fields, 'display_name', TestFields.display_name,
explicitly_set=False, inheritable=False, value='local default', default_value='local default')
options=TestFields.max_attempts.values
)
self.assert_field_values(
editable_fields, 'display_name', TestFields.display_name,
explicitly_set=False, inheritable=False, value='local default', default_value='local default'
)
editable_fields = self.get_descriptor({}).editable_metadata_fields
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
self.assert_field_values(
editable_fields, 'max_attempts', TestFields.max_attempts,
explicitly_set=False, inheritable=False, value=1000, default_value=1000, type='Integer',
options=TestFields.max_attempts.values)
options=TestFields.max_attempts.values
)
def test_inherited_field(self):
model_val = {'display_name' : 'inherited'}
model_val = {'display_name': 'inherited'}
descriptor = self.get_descriptor(model_val)
# Mimic an inherited value for display_name (inherited and inheritable are the same in this case).
descriptor._inherited_metadata = model_val
descriptor._inheritable_metadata = model_val
editable_fields = descriptor.editable_metadata_fields
self.assert_field_values(editable_fields, 'display_name', TestFields.display_name,
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited')
self.assert_field_values(
editable_fields, 'display_name', TestFields.display_name,
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited'
)
descriptor = self.get_descriptor({'display_name' : 'explicit'})
descriptor = self.get_descriptor({'display_name': 'explicit'})
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
descriptor._inheritable_metadata = {'display_name' : 'inheritable value'}
descriptor._inheritable_metadata = {'display_name': 'inheritable value'}
descriptor._inherited_metadata = {}
editable_fields = descriptor.editable_metadata_fields
self.assert_field_values(editable_fields, 'display_name', TestFields.display_name,
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value')
self.assert_field_values(
editable_fields, 'display_name', TestFields.display_name,
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value'
)
def test_type_and_options(self):
# test_display_name_field verifies that a String field is of type "Generic".
......@@ -92,23 +110,31 @@ class EditableMetadataFieldsTest(unittest.TestCase):
editable_fields = descriptor.editable_metadata_fields
# Tests for select
self.assert_field_values(editable_fields, 'string_select', TestFields.string_select,
self.assert_field_values(
editable_fields, 'string_select', TestFields.string_select,
explicitly_set=False, inheritable=False, value='default value', default_value='default value',
type='Select', options=[{'display_name' : 'first', 'value' : 'value a JSON'},
{'display_name' : 'second','value' : 'value b JSON'}])
type='Select', options=[{'display_name': 'first', 'value': 'value a JSON'},
{'display_name': 'second', 'value': 'value b JSON'}]
)
self.assert_field_values(editable_fields, 'float_select', TestFields.float_select,
self.assert_field_values(
editable_fields, 'float_select', TestFields.float_select,
explicitly_set=False, inheritable=False, value=.999, default_value=.999,
type='Select', options=[1.23, 0.98])
type='Select', options=[1.23, 0.98]
)
self.assert_field_values(editable_fields, 'boolean_select', TestFields.boolean_select,
self.assert_field_values(
editable_fields, 'boolean_select', TestFields.boolean_select,
explicitly_set=False, inheritable=False, value=None, default_value=None,
type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}])
type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}]
)
# Test for float
self.assert_field_values(editable_fields, 'float_non_select', TestFields.float_non_select,
self.assert_field_values(
editable_fields, 'float_non_select', TestFields.float_non_select,
explicitly_set=False, inheritable=False, value=.999, default_value=.999,
type='Float', options={'min': 0 , 'step' : .3})
type='Float', options={'min': 0, 'step': .3}
)
# Start of helper methods
......@@ -119,7 +145,6 @@ class EditableMetadataFieldsTest(unittest.TestCase):
def get_descriptor(self, model_data):
class TestModuleDescriptor(TestFields, XmlDescriptor):
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields
......
......@@ -34,7 +34,7 @@ class WordCloudFields(object):
"""XFields for word cloud."""
num_inputs = StringyInteger(
display_name="Inputs",
help="Number of text boxes for student to input words/sentences.",
help="Number of text boxes available for students to input words/sentences.",
scope=Scope.settings,
default=5,
values = {"min" : 1 }
......@@ -48,7 +48,7 @@ class WordCloudFields(object):
)
display_student_percents = StringyBoolean(
display_name="Show Percents",
help="Show statistics for entered words near every word separately at the top.",
help="Statistics are shown for entered words near that word.",
scope=Scope.settings,
default=True
)
......
......@@ -82,7 +82,7 @@ class HTMLSnippet(object):
class XModuleFields(object):
display_name = String(
display_name="Display Name",
help="Specifies the name for this component. The name appears as a tooltip in the course ribbon at the top of the page.",
help="This name appears in the horizontal navigation at the top of the page.",
scope=Scope.settings,
default=None
)
......
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