Commit 927433f5 by gradyward

Merge branch 'authoring' of https://github.com/edx/edx-ora2 into…

Merge branch 'authoring' of https://github.com/edx/edx-ora2 into grady/student-training-authoring-templates

Conflicts:
	openassessment/xblock/studio_mixin.py
parents b647d067 e132d976
{% load i18n %}
{% load tz %}
{% spaceless %}
<div id="openassessment-editor" class="editor-with-buttons editor-with-tabs">
<div class="openassessment_editor_content_and_tabs">
......@@ -30,15 +31,59 @@
</li>
<li class="openassessment_date_editor field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_start_editor" class="setting-label">{% trans "Response Submission Start Date"%} </label>
<input type="datetime-local" class="input setting-input" id="openassessment_submission_start_editor" value="{{ submission_start }}">
<label
for="openassessment_submission_start_date"
class="setting-label">
{% trans "Submission Start Date" %}
</label>
<input
type="text"
class="input setting-input"
id="openassessment_submission_start_date"
value="{{ submission_start|utc|date:"y-m-d" }}"
>
</div>
<div class="wrapper-comp-setting">
<label
for="openassessment_submission_start_time"
class="setting-label">
{% trans "Submission Start Time" %}
</label>
<input
type="text"
class="input setting-input"
id="openassessment_submission_start_time"
value="{{ submission_start|utc|date:"H:i" }}"
>
</div>
<p class="setting-help">{% trans "The date at which submissions will first be accepted." %}</p>
</li>
<li class="openassessment_date_editor field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="openassessment_submission_due_editor" class="setting-label">{% trans "Response Submission Due Date" %}</label>
<input type="datetime-local" class="input setting-input" id="openassessment_submission_due_editor" value="{{ submission_due }}">
<label
for="openassessment_submission_due_date"
class="setting-label">
{% trans "Submission Due Date" %}
</label>
<input
type="text"
class="input setting-input"
id="openassessment_submission_due_date"
value="{{ submission_due|utc|date:"y-m-d" }}"
>
</div>
<div class="wrapper-comp-setting">
<label
for="openassessment_submission_due_time"
class="setting-label">
{% trans "Submission Due Time" %}
</label>
<input
type="text"
class="input setting-input"
id="openassessment_submission_due_time"
value="{{ submission_due|utc|date:"H:i" }}"
>
</div>
<p class="setting-help">{% trans "The date at which submissions will stop being accepted." %}</p>
</li>
......
{% load i18n %}
{% load tz %}
{% spaceless %}
<li class="openassessment_assessment_module_settings_editor" id="oa_peer_assessment_editor">
<div class = "drag-handle action"></div>
......@@ -33,16 +34,44 @@
<li class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="peer_assessment_start_date" class="setting-label">{% trans "Start Date" %}</label>
<input id="peer_assessment_start_date" type="datetime-local" class="input setting-input" value="{{ assessments.peer_assessment.start }}">
<input
id="peer_assessment_start_date"
type="text"
class="input setting-input"
value="{{ assessments.peer_assessment.start|utc|date:"Y-m-d" }}"
>
</div>
<p class="setting-help">{% trans "If desired, specify a start date for the peer assessment period. If no date is specified, peer assessment can begin when submissions begin."%}</p>
<div class="wrapper-comp-setting">
<label for="peer_assessment_start_time" class="setting-label">{% trans "Start Time" %}</label>
<input
id="peer_assessment_start_time"
type="text"
class="input setting-input"
value="{{ assessments.peer_assessment.start|utc|date:"H:i" }}"
>
</div>
<p class="setting-help">{% trans "Specify a start date for the peer assessment period." %}</p>
</li>
<li class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="peer_assessment_due_date" class="setting-label">{% trans "Due Date" %}</label>
<input id="peer_assessment_due_date" type="datetime-local" class="input setting-input" value="{{ assessments.peer_assessment.due }}">
<input
id="peer_assessment_due_date"
type="text"
class="input setting-input"
value="{{ assessments.peer_assessment.due|utc|date:"Y-m-d" }}"
>
</div>
<div class="wrapper-comp-setting">
<label for="peer_assessment_due_time" class="setting-label">{% trans "Due Time" %}</label>
<input
id="peer_assessment_due_time"
type="text"
class="input setting-input"
value="{{ assessments.peer_assessment.due|utc|date:"H:i" }}"
>
</div>
<p class="setting-help">{% trans "If desired, specify a due date for the peer assessment period. If no date is specified, peer assessment can run as long as the problem is open."%}</p>
<p class="setting-help">{% trans "Specify a due date for the peer assessment period." %}</p>
</li>
</ul>
</div>
......
{% load i18n %}
{% load tz %}
{% spaceless %}
<li class="openassessment_assessment_module_settings_editor" id="oa_self_assessment_editor">
<div class = "drag-handle action"></div>
......@@ -19,16 +20,44 @@
<li class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="self_assessment_start_date" class="setting-label">{% trans "Start Date" %}</label>
<input id="self_assessment_start_date" type="datetime-local" class="input setting-input" value="{{ assessments.self_assessment.start }}">
<input
id="self_assessment_start_date"
type="text"
class="input setting-input"
value="{{ assessments.self_assessment.start|utc|date:"Y-m-d" }}"
>
</div>
<p class="setting-help">{% trans "If desired, specify a start date for the self assessment period. If no date is specified, self assessment can begin when submissions begin."%}</p>
<div class="wrapper-comp-setting">
<label for="self_assessment_start_time" class="setting-label">{% trans "Start Time" %}</label>
<input
id="self_assessment_start_time"
type="text"
class="input setting-input"
value="{{ assessments.self_assessment.start|utc|date:"H:i" }}"
>
</div>
<p class="setting-help">{% trans "Specify a start date for the self assessment period." %}</p>
</li>
<li class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label for="self_assessment_due_date" class="setting-label">{% trans "Due Date" %}</label>
<input id="self_assessment_due_date" type="datetime-local" class="input setting-input" value="{{ assessments.self_assessment.due }}">
<input
id="self_assessment_due_date"
type="text"
class="input setting-input"
value="{{ assessments.self_assessment.due|utc|date:"Y-m-d" }}"
>
</div>
<div class="wrapper-comp-setting">
<label for="self_assessment_due_time" class="setting-label">{% trans "Due Time" %}</label>
<input
id="self_assessment_due_time"
type="text"
class="input setting-input"
value="{{ assessments.self_assessment.due|utc|date:"H:i" }}"
>
</div>
<p class="setting-help">{% trans "If desired, specify a due date for the self assessment period. If no date is specified, self assessment can run as long as the problem is open."%}</p>
<p class="setting-help">{% trans "Specify a due date for the self assessment period." %}</p>
</li>
</ul>
</div>
......
......@@ -16,61 +16,8 @@
{% trans "Enter one or more sample responses that you've created, together with the scores you would give those responses. Be sure to format the responses and scores according to the placeholder text below." %}
</p>
<ol>
{% for example in assessments.training.examples%}
<li class="openassessment_training_example is-collapsible">
<div class="openassessment_training_example_header view-outline">
<a class="action expand-collapse collapse">
<i class="icon-caret-down ui-toggle-expansion"></i>
</a>
<h6 class="openassessment_training_example_header_title">
{% blocktrans %} Scored Response {% endblocktrans %}
</h6>
<div class="openassessment_training_example_remove">
<h2>{% trans "Remove" %}</h2>
</div>
</div>
<div class="openassessment_training_example_body">
<div class="openassessment_training_example_scored_rubric wrapper-comp-settings">
<h2>{% trans "Scored Rubric" %}</h2>
<ol class="openassessment_training_example_criteria_selections list-input settings-list">
{% for criterion in criteria %}
<li class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label class="openassessment_training_example_criterion_name setting-label">
{{criterion.name}}
<select class="openassessment_training_example_criterion_option setting-input">
<option value="Not Scored">{% trans "Not Scored" %}</option>
{% for option in criterion.options %}
<option value={{option.name}}
{% for opt_sel in example.options_selected %}
{% if opt_sel.criterion == criterion.name and opt_sel.option == option.name %}
selected
{% endif %}
{% endfor %}
>
{{option.name}} - {{option.points}} {% trans "points" %}
</option>
{% endfor %}
</select>
</label>
</div>
</li>
{% endfor %}
</ol>
</div>
<div class="openassessment_training_example_essay_wrapper">
<h2>
{% trans "Response" %}
</h2>
<textarea class="openassessment_training_example_essay">
{{example.answer}}
</textarea>
</div>
</div>
</li>
{% for example in assessments.training.examples %}
{% include "openassessmentblock/edit/oa_training_example.html" with example=example %}
{% endfor %}
</ol>
<div>
......@@ -80,4 +27,8 @@
</div>
</div>
</li>
<div id="openassessment_training_example_template" class="is--hidden">
{% include "openassessmentblock/edit/oa_training_example.html" with example=assessments.training.template %}
</div>
{% endspaceless %}
\ No newline at end of file
{% load i18n %}
{% spaceless %}
<li class="openassessment_training_example is-collapsible">
<div class="openassessment_training_example_header view-outline">
<a class="action expand-collapse collapse">
<i class="icon-caret-down ui-toggle-expansion"></i>
</a>
<h6 class="openassessment_training_example_header_title">
{% blocktrans %} Scored Response {% endblocktrans %}
</h6>
<div class="openassessment_training_example_remove">
<h2>{% trans "Remove" %}</h2>
</div>
</div>
<div class="openassessment_training_example_body">
<div class="openassessment_training_example_scored_rubric wrapper-comp-settings">
<h2>{% trans "Scored Rubric" %}</h2>
<ol class="openassessment_training_example_criteria_selections list-input settings-list">
{% for criterion in example.criteria %}
<li class="field comp-setting-entry">
<div class="wrapper-comp-setting">
<label class="openassessment_training_example_criterion_name setting-label">
{{criterion.name}}
<select class="openassessment_training_example_criterion_option setting-input">
<option value="Not Scored">{% trans "Not Scored" %}</option>
{% for option in criterion.options %}
<option value={{option.name}}
{% if criterion.option_selected == option.name %} selected {% endif %}
>
{{option.name}} - {{option.points}} {% trans "points" %}
</option>
{% endfor %}
</select>
</label>
</div>
</li>
{% endfor %}
</ol>
</div>
<div class="openassessment_training_example_essay_wrapper">
<h2>{% trans "Response" %}</h2>
<textarea class="openassessment_training_example_essay">{{example.answer}}</textarea>
</div>
</div>
</li>
{% endspaceless %}
\ No newline at end of file
......@@ -5,7 +5,6 @@ Schema for validating and sanitizing data received from the JavaScript client.
import dateutil
from pytz import utc
from voluptuous import Schema, Required, All, Any, Range, In, Invalid
from openassessment.xblock.xml import parse_examples_xml_str, UpdateFromXmlError
def utf8_validator(value):
......@@ -57,25 +56,6 @@ def datetime_validator(value):
raise Invalid(u"Could not parse datetime from value \"{val}\"".format(val=value))
def examples_xml_validator(value):
"""Parse and validate student training examples XML.
Args:
value: The value to parse.
Returns:
list of training examples, serialized as dictionaries.
Raises:
Invalid
"""
try:
return parse_examples_xml_str(value)
except UpdateFromXmlError:
raise Invalid(u"Could not parse examples from XML")
# Schema definition for an update from the Studio JavaScript editor.
EDITOR_UPDATE_SCHEMA = Schema({
Required('prompt'): utf8_validator,
......@@ -98,7 +78,17 @@ EDITOR_UPDATE_SCHEMA = Schema({
Required('due', default=None): Any(datetime_validator, None),
'must_grade': All(int, Range(min=0)),
'must_be_graded_by': All(int, Range(min=0)),
'examples': All(utf8_validator, examples_xml_validator)
'examples': [
Schema({
Required('answer'): utf8_validator,
Required('options_selected'): [
Schema({
Required('criterion'): utf8_validator,
Required('option'): utf8_validator
})
]
})
]
})
],
Required('feedbackprompt', default=u""): utf8_validator,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -437,7 +437,8 @@
],
"assessments": {
"peer_assessment": {
"start": "2014-10-04T00:00:00",
"start": "",
"due": "",
"must_grade": 5,
"must_be_graded_by": 3
},
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -26,7 +26,7 @@ describe("OpenAssessment.Container", function () {
templateElement: $("#template").get(0),
addButtonElement: $("#add_button").get(0),
removeButtonClass: "remove_button",
containerItemClass: "test_item",
containerItemClass: "container_item",
}
);
};
......@@ -40,8 +40,10 @@ describe("OpenAssessment.Container", function () {
// so we just define the fixture inline.
setFixtures(
'<div id="container" />' +
'<div id="template" test_id="">' +
'<div class="remove_button" />' +
'<div id="template">' +
'<div class="container_item" test_id="">' +
'<div class="remove_button" />' +
'</div>' +
'</div>' +
'<div id="add_button" />'
);
......@@ -142,7 +144,7 @@ describe("OpenAssessment.Container", function () {
// Add an item directly to the container element in the DOM,
// before initializing the container object.
$("#container").append(
'<div class="test_item" test_id="0">' +
'<div class="container_item" test_id="0">' +
'<div class="remove_button" />' +
'<div>'
);
......
......@@ -36,8 +36,8 @@ describe("OpenAssessment edit assessment views", function() {
it("Loads a description", function() {
view.mustGradeNum(1);
view.mustBeGradedByNum(2);
view.startDatetime("2014-01-01T00:00");
view.dueDatetime("2014-03-04T00:00");
view.startDatetime("2014-01-01", "00:00");
view.dueDatetime("2014-03-04", "00:00");
expect(view.description()).toEqual({
must_grade: 1,
must_be_graded_by: 2,
......@@ -65,8 +65,8 @@ describe("OpenAssessment edit assessment views", function() {
it("Enables and disables", function() { testEnableAndDisable(view); });
it("Loads a description", function() {
view.startDatetime("2014-01-01T00:00");
view.dueDatetime("2014-03-04T00:00");
view.startDatetime("2014-01-01", "00:00");
view.dueDatetime("2014-03-04", "00:00");
expect(view.description()).toEqual({
start: "2014-01-01T00:00",
due: "2014-03-04T00:00"
......@@ -74,8 +74,8 @@ describe("OpenAssessment edit assessment views", function() {
});
it("Handles default dates", function() {
view.startDatetime("");
view.dueDatetime("");
view.startDatetime("", "");
view.dueDatetime("", "");
expect(view.description().start).toBe(null);
expect(view.description().due).toBe(null);
});
......@@ -90,7 +90,8 @@ describe("OpenAssessment edit assessment views", function() {
});
it("Enables and disables", function() { testEnableAndDisable(view); });
it("Loads a description", function() { testLoadXMLExamples(view); });
// This test was deleted because it is obsolete due to Context Changes, but has not yet been
// Replaced by functionality in the JavaScript (which will be tested)
});
describe("OpenAssessment.EditExampleBasedAssessmentView", function() {
......
......@@ -47,17 +47,17 @@ describe("OpenAssessment.EditSettingsView", function() {
});
it("sets and loads the submission start/due dates", function() {
view.submissionStart("");
view.submissionStart("", "");
expect(view.submissionStart()).toBe(null);
view.submissionStart("2014-04-01T00:00.0000Z");
expect(view.submissionStart()).toEqual("2014-04-01T00:00.0000Z");
view.submissionStart("2014-04-01", "00:00");
expect(view.submissionStart()).toEqual("2014-04-01T00:00");
view.submissionDue("");
view.submissionDue("", "");
expect(view.submissionDue()).toBe(null);
view.submissionDue("2014-05-02T00:00.0000Z");
expect(view.submissionDue()).toEqual("2014-05-02T00:00.0000Z");
view.submissionDue("2014-05-02", "00:00");
expect(view.submissionDue()).toEqual("2014-05-02T00:00");
});
it("sets and loads the image enabled state", function() {
......
......@@ -10,11 +10,19 @@ For example, to create a container for an item called "test_item",
the DOM should look something like:
<div id="test_container" />
<div id="test_item_template">
<div class="test_item_remove_button">Remove</div>
<p>This is the default value for the item.</p>
<div class="test_item">
<div class="test_item_remove_button">Remove</div>
<p>This is the default value for the item.</p>
</div>
</div>
<div id="test_item_add_button">Add</div>
A critical property of this setup is that the element you want to
include/duplicate is wrapped inside of a template element which is
the one that your reference when referring to a template. In the
above example, $("#test_item_template") would be the appropriate
reference to the template.
You can then initialize the container:
>>> var container = $("#test_container").get(0);
>>> var template = $("#test_item_template").get(0);
......@@ -82,7 +90,10 @@ OpenAssessment.Container.prototype = {
// Copy the template into the container
// Remove any CSS IDs (since now the element is not unique)
// and add the item class so we can find it later.
// Note that the element we add is the first child of the template element.
// For more on the template structure expected, see the class comment
$(this.templateElement)
.children().first()
.clone()
.removeAttr('id')
.toggleClass('is--hidden', false)
......
......@@ -19,8 +19,8 @@ OpenAssessment.StudioView = function(runtime, element, server) {
// Resize the editing modal
this.fixModalHeight();
// Initialize the tabs
$(".openassessment_editor_content_and_tabs", this.element).tabs();
// Initializes the tabbing functionality and activates the last used.
this.initializeTabs();
// Initialize the prompt tab view
this.promptView = new OpenAssessment.EditPromptView(
......@@ -82,6 +82,36 @@ OpenAssessment.StudioView.prototype = {
},
/**
Initializes the tabs that seperate the sections of the editor.
Because this function relies on the OpenAssessment Name space, the tab that it first
active will be the one that the USER was presented with, regardless of which editor they
were using. I.E. If I leave Editor A in the settings state, and enter editor B, editor B
will automatically open with the settings state.
**/
initializeTabs: function() {
// If this is the first editor that the user has opened, default to the prompt view.
if (typeof(OpenAssessment.lastOpenEditingTab) === "undefined") {
OpenAssessment.lastOpenEditingTab = 0;
}
// Initialize JQuery UI Tabs, and activates the appropriate tab.
$(".openassessment_editor_content_and_tabs", this.element)
.tabs()
.tabs('option', 'active', OpenAssessment.lastOpenEditingTab);
},
/**
Saves the state of the editing tabs in a variable outside of the scope of the editor.
When the user reopens the editing view, they will be greeted by the same tab that they left.
This code is called by the two paths that we could exit the modal through: Saving and canceling.
**/
saveTabState: function() {
var tabElement = $(".openassessment_editor_content_and_tabs", this.element);
OpenAssessment.lastOpenEditingTab = tabElement.tabs('option', 'active');
},
/**
Installs click listeners which initialize drag and drop functionality for assessment modules.
**/
initializeSortableAssessments: function () {
......@@ -125,7 +155,7 @@ OpenAssessment.StudioView.prototype = {
**/
save: function () {
var view = this;
this.saveTabState();
// Check whether the problem has been released; if not,
// warn the user and allow them to cancel.
this.server.checkReleased().done(
......@@ -187,7 +217,8 @@ OpenAssessment.StudioView.prototype = {
Cancel editing.
**/
cancel: function () {
// Notify the client-side runtime so it will close the editing modal.
// Notify the client-side runtime so it will close the editing modal
this.saveTabState();
this.runtime.notify('cancel', {});
},
......
/**
Show and hide elements based on a checkbox.
Args:
element (DOM element): The parent element, used to scope the selectors.
hiddenSelector (string): The CSS selector string for elements
to show when the checkbox is in the "off" state.
shownSelector (string): The CSS selector string for elements
to show when the checkbox is in the "on" state.
**/
OpenAssessment.ToggleControl = function(element, hiddenSelector, shownSelector) {
this.element = element;
this.hiddenSelector = hiddenSelector;
this.shownSelector = shownSelector;
};
OpenAssessment.ToggleControl.prototype = {
/**
Install the event handler for the checkbox,
passing in the toggle control object as the event data.
Args:
checkboxSelector (string): The CSS selector string for the checkbox.
Returns:
OpenAssessment.ToggleControl
**/
install: function(checkboxSelector) {
$(checkboxSelector, this.element).change(
this, function(event) {
var control = event.data;
if (this.checked) { control.show(); }
else { control.hide(); }
}
);
return this;
},
show: function() {
$(this.hiddenSelector, this.element).addClass('is--hidden');
$(this.shownSelector, this.element).removeClass('is--hidden');
},
hide: function() {
$(this.hiddenSelector, this.element).removeClass('is--hidden');
$(this.shownSelector, this.element).addClass('is--hidden');
}
};
/**
Interface for editing peer assessment settings.
Args:
......@@ -62,11 +12,25 @@ OpenAssessment.EditPeerAssessmentView = function(element) {
this.element = element;
this.name = "peer-assessment";
// Configure the toggle checkbox to enable/disable this assessment
new OpenAssessment.ToggleControl(
this.element,
"#peer_assessment_description_closed",
"#peer_assessment_settings_editor"
).install("#include_peer_assessment");
// Configure the date and time fields
this.startDatetimeControl = new OpenAssessment.DatetimeControl(
this.element,
"#peer_assessment_start_date",
"#peer_assessment_start_time"
).install();
this.dueDatetimeControl = new OpenAssessment.DatetimeControl(
this.element,
"#peer_assessment_due_date",
"#peer_assessment_due_time"
).install();
};
OpenAssessment.EditPeerAssessmentView.prototype = {
......@@ -141,28 +105,28 @@ OpenAssessment.EditPeerAssessmentView.prototype = {
Get or set the start date and time of the assessment.
Args:
datetime (string, optional): If provided, set the datetime to this value.
dateString (string, optional): If provided, set the date (YY-MM-DD).
timeString (string, optional): If provided, set the time (HH:MM, 24-hour clock).
Returns:
string (ISO-formatted UTC datetime)
**/
startDatetime: function(datetime) {
var sel = $("#peer_assessment_start_date", this.element);
return OpenAssessment.Fields.datetimeField(sel, datetime);
startDatetime: function(dateString, timeString) {
return this.startDatetimeControl.datetime(dateString, timeString);
},
/**
Get or set the due date and time of the assessment.
Args:
datetime (string, optional): If provided, set the datetime to this value.
dateString (string, optional): If provided, set the date (YY-MM-DD).
timeString (string, optional): If provided, set the time (HH:MM, 24-hour clock).
Returns:
string (ISO-formatted UTC datetime)
**/
dueDatetime: function(datetime) {
var sel = $("#peer_assessment_due_date", this.element);
return OpenAssessment.Fields.datetimeField(sel, datetime);
dueDatetime: function(dateString, timeString) {
return this.dueDatetimeControl.datetime(dateString, timeString);
},
/**
......@@ -191,11 +155,25 @@ OpenAssessment.EditSelfAssessmentView = function(element) {
this.element = element;
this.name = "self-assessment";
// Configure the toggle checkbox to enable/disable this assessment
new OpenAssessment.ToggleControl(
this.element,
"#self_assessment_description_closed",
"#self_assessment_settings_editor"
).install("#include_self_assessment");
// Configure the date and time fields
this.startDatetimeControl = new OpenAssessment.DatetimeControl(
this.element,
"#self_assessment_start_date",
"#self_assessment_start_time"
).install();
this.dueDatetimeControl = new OpenAssessment.DatetimeControl(
this.element,
"#self_assessment_due_date",
"#self_assessment_due_time"
).install();
};
OpenAssessment.EditSelfAssessmentView.prototype = {
......@@ -239,28 +217,28 @@ OpenAssessment.EditSelfAssessmentView.prototype = {
Get or set the start date and time of the assessment.
Args:
datetime (string, optional): If provided, set the datetime to this value.
dateString (string, optional): If provided, set the date (YY-MM-DD).
timeString (string, optional): If provided, set the time (HH:MM, 24-hour clock).
Returns:
string (ISO-formatted UTC datetime)
**/
startDatetime: function(datetime) {
var sel = $("#self_assessment_start_date", this.element);
return OpenAssessment.Fields.datetimeField(sel, datetime);
startDatetime: function(dateString, timeString) {
return this.startDatetimeControl.datetime(dateString, timeString);
},
/**
Get or set the due date and time of the assessment.
Args:
datetime (string, optional): If provided, set the datetime to this value.
dateString (string, optional): If provided, set the date (YY-MM-DD).
timeString (string, optional): If provided, set the time (HH:MM, 24-hour clock).
Returns:
string (ISO-formatted UTC datetime)
**/
dueDatetime: function(datetime) {
var sel = $("#self_assessment_due_date", this.element);
return OpenAssessment.Fields.datetimeField(sel, datetime);
dueDatetime: function(dateString, timeString) {
return this.dueDatetimeControl.datetime(dateString, timeString);
},
/**
......
......@@ -7,12 +7,6 @@ OpenAssessment.Fields = {
return sel.val();
},
datetimeField: function(sel, value) {
if (typeof(value) !== "undefined") { sel.val(value); }
var fieldValue = sel.val();
return (fieldValue !== "") ? fieldValue : null;
},
intField: function(sel, value) {
if (typeof(value) !== "undefined") { sel.val(value); }
return parseInt(sel.val(), 10);
......@@ -23,3 +17,113 @@ OpenAssessment.Fields = {
return sel.prop("checked");
},
};
/**
Show and hide elements based on a checkbox.
Args:
element (DOM element): The parent element, used to scope the selectors.
hiddenSelector (string): The CSS selector string for elements
to show when the checkbox is in the "off" state.
shownSelector (string): The CSS selector string for elements
to show when the checkbox is in the "on" state.
**/
OpenAssessment.ToggleControl = function(element, hiddenSelector, shownSelector) {
this.element = element;
this.hiddenSelector = hiddenSelector;
this.shownSelector = shownSelector;
};
OpenAssessment.ToggleControl.prototype = {
/**
Install the event handler for the checkbox,
passing in the toggle control object as the event data.
Args:
checkboxSelector (string): The CSS selector string for the checkbox.
Returns:
OpenAssessment.ToggleControl
**/
install: function(checkboxSelector) {
$(checkboxSelector, this.element).change(
this, function(event) {
var control = event.data;
if (this.checked) { control.show(); }
else { control.hide(); }
}
);
return this;
},
show: function() {
$(this.hiddenSelector, this.element).addClass('is--hidden');
$(this.shownSelector, this.element).removeClass('is--hidden');
},
hide: function() {
$(this.hiddenSelector, this.element).removeClass('is--hidden');
$(this.shownSelector, this.element).addClass('is--hidden');
}
};
/**
Date and time input fields.
Args:
element (DOM element): The parent element of the control inputs.
datePicker (string): The CSS selector for the date input field.
timePicker (string): The CSS selector for the time input field.
**/
OpenAssessment.DatetimeControl = function(element, datePicker, timePicker) {
this.element = element;
this.datePicker = datePicker;
this.timePicker = timePicker;
};
OpenAssessment.DatetimeControl.prototype = {
/**
Configure the date and time picker inputs.
Returns:
OpenAssessment.DatetimeControl
**/
install: function() {
var dateString = $(this.datePicker, this.element).val();
$(this.datePicker, this.element).datepicker({ showButtonPanel: true })
.datepicker("option", "dateFormat", "yy-mm-dd")
.datepicker("setDate", dateString);
$(this.timePicker, this.element).timepicker({
timeFormat: 'H:i',
step: 60
});
return this;
},
/**
Get or set the date and time.
Args:
dateString (string, optional): If provided, set the date (YY-MM-DD).
timeString (string, optional): If provided, set the time (HH:MM, 24-hour clock).
Returns:
ISO-formatted datetime string.
**/
datetime: function(dateString, timeString) {
var datePickerSel = $(this.datePicker, this.element);
var timePickerSel = $(this.timePicker, this.element);
if (typeof(dateString) !== "undefined") { datePickerSel.datepicker("setDate", dateString); }
if (typeof(timeString) !== "undefined") { timePickerSel.val(timeString); }
if (datePickerSel.val() === "" && timePickerSel.val() === "") {
return null;
}
return datePickerSel.val() + "T" + timePickerSel.val();
}
};
\ No newline at end of file
......@@ -13,6 +13,19 @@ OpenAssessment.EditSettingsView = function(element, assessmentViews) {
this.settingsElement = element;
this.assessmentsElement = $(element).siblings('#openassessment_assessment_module_settings_editors').get(0);
this.assessmentViews = assessmentViews;
// Configure the date and time fields
this.startDatetimeControl = new OpenAssessment.DatetimeControl(
this.element,
"#openassessment_submission_start_date",
"#openassessment_submission_start_time"
).install();
this.dueDatetimeControl = new OpenAssessment.DatetimeControl(
this.element,
"#openassessment_submission_due_date",
"#openassessment_submission_due_time"
).install();
};
......@@ -37,30 +50,30 @@ OpenAssessment.EditSettingsView.prototype = {
Get or set the submission start date.
Args:
datetime (string, optional): If provided, set the datetime.
dateString (string, optional): If provided, set the date (YY-MM-DD).
timeString (string, optional): If provided, set the time (HH:MM, 24-hour clock).
Returns:
string (ISO-format UTC datetime)
**/
submissionStart: function(datetime) {
var sel = $("#openassessment_submission_start_editor", this.settingsElement);
return OpenAssessment.Fields.datetimeField(sel, datetime);
submissionStart: function(dateString, timeString) {
return this.startDatetimeControl.datetime(dateString, timeString);
},
/**
Get or set the submission end date.
Args:
datetime (string, optional): If provided, set the datetime.
dateString (string, optional): If provided, set the date (YY-MM-DD).
timeString (string, optional): If provided, set the time (HH:MM, 24-hour clock).
Returns:
string (ISO-format UTC datetime)
**/
submissionDue: function(datetime) {
var sel = $("#openassessment_submission_start_editor", this.settingsElement);
return OpenAssessment.Fields.datetimeField(sel, datetime);
submissionDue: function(dateString, timeString) {
return this.dueDatetimeControl.datetime(dateString, timeString);
},
/**
......
......@@ -747,7 +747,7 @@
width: 58%;
display: inline-block;
position: absolute;
left: 10px;
left: 15px;
height: 100%;
textarea {
min-height: 90px;
......@@ -772,6 +772,7 @@
width: 40%;
display: inline-block;
float: right;
min-height: 150px;
.openassessment_training_example_criteria_selections {
.comp-setting-entry {
.wrapper-comp-setting{
......
......@@ -7,6 +7,8 @@ import copy
import logging
from django.template.loader import get_template
from django.utils.translation import ugettext as _
from dateutil.parser import parse as parse_date
import pytz
from voluptuous import MultipleInvalid
from xblock.core import XBlock
from xblock.fragment import Fragment
......@@ -14,6 +16,7 @@ from openassessment.xblock import xml
from openassessment.xblock.validation import validator
from openassessment.xblock.data_conversion import create_rubric_dict
from openassessment.xblock.schema import EDITOR_UPDATE_SCHEMA
from openassessment.xblock.resolve_dates import resolve_dates
logger = logging.getLogger(__name__)
......@@ -62,33 +65,21 @@ class StudioMixin(object):
'assessments (dict)
"""
# Copies the rubric assessments so that we can change student
# training examples from dict -> str without negatively modifying
# the openassessmentblock definition.
# Django Templates cannot handle dict keys with dashes, so we'll convert
# the dashes to underscores.
# used_assessments (and its unused counterpart) are lists intended to indicate
# the order that settings editors should be rendered. Using lists allows a set order
# which django can easily convert into template names.
used_assessments = []
assessments = {}
for assessment in self.rubric_assessments:
name = assessment['name'].replace('-', '_')
used_assessments.append(name)
assessments[name] = copy.deepcopy(assessment)
unused_assessments = {'student_training', 'peer_assessment', 'self_assessment', 'example_based_assessment'}
unused_assessments = unused_assessments - set(used_assessments)
student_training_module = self.get_assessment_module(
'student-training'
# In the authoring GUI, date and time fields should never be null.
# Therefore, we need to resolve all "default" dates to datetime objects
# before displaying them in the editor.
__, __, date_ranges = resolve_dates(
self.start, self.due,
[(self.submission_start, self.submission_due)] +
[(asmnt.get('start'), asmnt.get('due')) for asmnt in self.valid_assessments]
)
if student_training_module:
assessments['training'] = copy.deepcopy(student_training_module)
submission_due = self.submission_due if self.submission_due else ''
submission_start = self.submission_start if self.submission_start else ''
submission_start, submission_due = date_ranges[0]
assessments = self._assessments_editor_context(date_ranges[1:])
used_assessments = set(assessments.keys()) - {'training'}
all_assessments = set(['student_training', 'peer_assessment', 'self_assessment', 'example_based_assessment'])
unused_assessments = all_assessments - used_assessments
# Every rubric requires one criterion. If there is no criteria
# configured for the XBlock, return one empty default criterion, with
......@@ -104,7 +95,7 @@ class StudioMixin(object):
'submission_start': submission_start,
'assessments': assessments,
'criteria': criteria,
'feedbackprompt': unicode(self.rubric_feedback_prompt),
'feedbackprompt': self.rubric_feedback_prompt,
'unused_assessments': unused_assessments,
'used_assessments': used_assessments
}
......@@ -182,3 +173,59 @@ class StudioMixin(object):
'success': True, 'msg': u'',
'is_released': self.is_released()
}
def _assessments_editor_context(self, assessment_dates):
"""
Transform the rubric assessments list into the context
we will pass to the Django template.
Args:
assessment_dates: List of assessment date ranges (tuples of start/end datetimes).
Returns:
dict
"""
assessments = {}
for asmnt, date_range in zip(self.rubric_assessments, assessment_dates):
# Django Templates cannot handle dict keys with dashes, so we'll convert
# the dashes to underscores.
name = asmnt['name']
template_name = name.replace('-', '_')
assessments[template_name] = copy.deepcopy(asmnt)
assessments[template_name]['start'] = date_range[0]
assessments[template_name]['due'] = date_range[1]
# In addition to the data in the student training assessment, we need to include two additional
# pieces of information: a blank context to render the empty template with, and the criteria
# for each example (so we don't have any complicated logic within the template). Though this
# could be accomplished within the template, we are opting to remove logic from the template.
student_training_module = self.get_assessment_module('student-training')
student_training_template = {'answer': ""}
criteria_list = copy.deepcopy(self.rubric_criteria)
for criterion in criteria_list:
criterion['option_selected'] = ""
student_training_template['criteria'] = criteria_list
if student_training_module:
example_list = []
# Adds each example to a modified version of the student training module dictionary.
for example in student_training_module['examples']:
criteria_list = copy.deepcopy(self.rubric_criteria)
# Equivalent to a Join Query, this adds the selected option to the Criterion's dictionary, so that
# it can be easily referenced in the template without searching through the selected options.
for criterion in criteria_list:
for option_selected in example['options_selected']:
if option_selected['criterion'] == criterion['name']:
criterion['option_selected'] = option_selected['option']
example_list.append({
'answer': example['answer'],
'criteria': criteria_list,
})
assessments['training'] = {'examples': example_list, 'template': student_training_template}
# If we don't have student training enabled, we still need to render a single (empty, or default) example
else:
assessments['training'] = {'examples': [student_training_template], 'template': student_training_template}
return assessments
{
"student_training_one_example": {
"xml": [
"<examples>",
"<example>",
"<answer>ẗëṡẗ äṅṡẅëṛ</answer>",
"<select criterion=\"Test criterion\" option=\"Yes\" />",
"</example>"
"</example>",
"</examples>"
],
"examples": [
{
......@@ -21,6 +23,7 @@
"student_training_multiple_examples": {
"xml": [
"<examples>",
"<example>",
"<answer>ẗëṡẗ äṅṡẅëṛ</answer>",
"<select criterion=\"Test criterion\" option=\"Yes\" />",
......@@ -30,7 +33,8 @@
"<answer>äṅöẗḧëṛ ẗëṡẗ äṅṡẅëṛ</answer>",
"<select criterion=\"Another test criterion\" option=\"Yes\" />",
"<select criterion=\"Test criterion\" option=\"No\" />",
"</example>"
"</example>",
"</examples>"
],
"examples": [
{
......
......@@ -85,5 +85,61 @@
"due": null
}
]
},
"student_training": {
"criteria": [
{
"order_num": 0,
"name": "тєѕт ¢яιтєяιση",
"prompt": "Test criterion prompt",
"options": [
{
"order_num": 0,
"points": 0,
"name": "Ṅö",
"explanation": "Ṅö explanation"
},
{
"order_num": 1,
"points": 2,
"name": "sǝʎ",
"explanation": "sǝʎ explanation"
}
],
"feedback": "required"
}
],
"prompt": "My new prompt.",
"feedback_prompt": "Feedback prompt",
"submission_due": "4014-02-27T09:46",
"submission_start": "4014-02-10T09:46",
"title": "My new title.",
"assessments": [
{
"name": "student-training",
"examples": [
{
"answer": "Ṫḧïṡ ïṡ äṅ äṅṡẅëṛ",
"options_selected": [
{ "criterion": "тєѕт ¢яιтєяιση", "option": "Ṅö" }
]
},
{
"answer": "This is another answer",
"options_selected": [
{ "criterion": "тєѕт ¢яιтєяιση", "option": "sǝʎ" }
]
}
]
},
{
"name": "peer-assessment",
"must_grade": 5,
"must_be_graded_by": 3,
"start": null,
"due": "4014-03-10T00:00"
}
]
}
}
......@@ -4,7 +4,6 @@ View-level tests for Studio view of OpenAssessment XBlock.
import json
import datetime as dt
import lxml.etree as etree
import pytz
from ddt import ddt, file_data
from .base import scenario, XBlockHandlerTestCase
......@@ -31,7 +30,13 @@ class StudioViewTest(XBlockHandlerTestCase):
@file_data('data/invalid_update_xblock.json')
@scenario('data/basic_scenario.xml')
def test_update_context_invalid_request_data(self, xblock, data):
expected_error = data.pop('expected_error')
# All schema validation errors have the same error message, so use that as the default
# Remove the expected error from the dictionary so we don't get an unexpected key error.
if 'expected_error' in data:
expected_error = data.pop('expected_error')
else:
expected_error = 'error updating xblock configuration'
xblock.published_date = None
resp = self.request(xblock, 'update_editor_context', json.dumps(data), response_format='json')
self.assertFalse(resp['success'])
......
......@@ -12,8 +12,8 @@ from django.test import TestCase
import ddt
from openassessment.xblock.openassessmentblock import OpenAssessmentBlock
from openassessment.xblock.xml import (
serialize_content, parse_from_xml_str, parse_rubric_xml_str,
parse_examples_xml_str, parse_assessments_xml_str,
serialize_content, parse_from_xml_str, parse_rubric_xml,
parse_examples_xml, parse_assessments_xml,
serialize_rubric_to_xml_str, serialize_examples_to_xml_str,
serialize_assessments_to_xml_str, UpdateFromXmlError
)
......@@ -335,7 +335,8 @@ class TestParseRubricFromXml(TestCase):
@ddt.file_data("data/parse_rubric_xml.json")
def test_parse_rubric_from_xml(self, data):
rubric = parse_rubric_xml_str("".join(data['xml']))
xml = etree.fromstring("".join(data['xml']))
rubric = parse_rubric_xml(xml)
self.assertEqual(rubric['prompt'], data['prompt'])
self.assertEqual(rubric['feedbackprompt'], data['feedbackprompt'])
......@@ -347,8 +348,8 @@ class TestParseExamplesFromXml(TestCase):
@ddt.file_data("data/parse_examples_xml.json")
def test_parse_examples_from_xml(self, data):
examples = parse_examples_xml_str("".join(data['xml']))
xml = etree.fromstring("".join(data['xml']))
examples = parse_examples_xml(xml)
self.assertEqual(examples, data['examples'])
@ddt.ddt
......@@ -356,8 +357,8 @@ class TestParseAssessmentsFromXml(TestCase):
@ddt.file_data("data/parse_assessments_xml.json")
def test_parse_assessments_from_xml(self, data):
assessments = parse_assessments_xml_str("".join(data['xml']))
xml = etree.fromstring("".join(data['xml']))
assessments = parse_assessments_xml(xml)
self.assertEqual(assessments, data['assessments'])
......@@ -401,4 +402,3 @@ class TestUpdateFromXml(TestCase):
def test_parse_from_xml_error(self, data):
with self.assertRaises(UpdateFromXmlError):
parse_from_xml_str("".join(data['xml']))
......@@ -750,70 +750,6 @@ def parse_from_xml_str(xml):
return parse_from_xml(_unicode_to_xml(xml))
def parse_rubric_xml_str(xml):
"""
Create a dictionary representation of the OpenAssessment XBlock rubric from
the given XML string.
Args:
xml (unicode): The XML definition of the XBlock's rubric.
Returns:
A dictionary of all rubric configuration.
Raises:
UpdateFromXmlError: The XML definition is invalid.
InvalidRubricError: The rubric was not semantically valid.
"""
return parse_rubric_xml(_unicode_to_xml(xml))
def parse_assessments_xml_str(xml):
"""
Create a dictionary representation of the OpenAssessment XBlock assessments
from the given XML string.
Args:
xml (unicode): The XML definition of the XBlock's assessments.
Returns:
A list of dictionaries representing the deserialized XBlock
configuration for each assessment module.
Raises:
UpdateFromXmlError: The XML definition is invalid.
InvalidAssessmentsError: The assessments are not semantically valid.
"""
return parse_assessments_xml(_unicode_to_xml(xml))
def parse_examples_xml_str(xml):
"""
Create a list representation of the OpenAssessment XBlock assessment
examples from the given XML string.
Args:
xml (unicode): The XML definition of the Assessment module's examples.
Returns:
A list of dictionaries representing the deserialized XBlock
configuration for each assessment example.
Raises:
UpdateFromXmlError: The XML definition is invalid.
"""
# This should work for both wrapped and unwrapped examples. Based on our final configuration (and tests)
# we should handle both cases gracefully.
if "<examples>" not in xml:
xml = u"<examples>" + xml + u"</examples>"
return parse_examples_xml(list(_unicode_to_xml(xml).findall('example')))
def _unicode_to_xml(xml):
"""
Converts unicode string to XML node.
......
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