Commit a4c57c94 by Dmitry Viskov

1. New auxiliary view "grade_available_responses_view" which displays the staff grading area

2. New auxiliary view for course ORA blocks listing (based on backgrid.js)
parent 8af51c16
...@@ -145,6 +145,12 @@ ...@@ -145,6 +145,12 @@
"gettext", "gettext",
"ngettext", "ngettext",
// Backgrid library
"Backgrid",
// XBlock dependencies
"XBlock",
// ORA-specific globals // ORA-specific globals
"OpenAssessment" "OpenAssessment"
] ]
......
...@@ -30,6 +30,8 @@ module.exports = function(config) { ...@@ -30,6 +30,8 @@ module.exports = function(config) {
'js/lib/jquery.timepicker.min.js', 'js/lib/jquery.timepicker.min.js',
'js/lib/jquery-ui-1.10.4.min.js', 'js/lib/jquery-ui-1.10.4.min.js',
'js/lib/underscore-min.js', 'js/lib/underscore-min.js',
'../../../node_modules/backbone/backbone.js',
'js/lib/backgrid.min.js',
'../../../node_modules/requirejs/require.js', '../../../node_modules/requirejs/require.js',
'../../../require-config.js', '../../../require-config.js',
{ {
......
{% load i18n %}
{% spaceless %}
<div class="wrapper wrapper--xblock wrapper--openassessment theme--basic">
<div class="openassessment problem">
<div class="wrapper--grid">
{% if title %}
<h4 class="openassessment__title problem__header">{% trans title %}</h4>
{% endif %}
{% if staff_assessment_required %}
<div class="openassessment__staff-area">
<div class="openassessment__staff-grading wrapper--staff-grading">
<div class="staff-grading ui-staff">
<h5 class="staff-grading__title">
<span class="staff-info__title__copy">{% trans "Grade Available Responses" %}</span>
</h5>
<div class="staff-info__content ui-staff__content">
{% include "openassessmentblock/staff_area/oa_staff_grade_learners.html" with staff_assessment_ungraded=staff_assessment_ungraded staff_assessment_in_progress=staff_assessment_in_progress %}
</div>
</div>
</div>
</div>
{% else %}
<div class="openassessment__staff-area-unavailable">
{% trans "Grade Available Responses is unavailable. This item is not configured for Staff Assessment." %}
</div>
{% endif %}
</div>
</div>
</div>
{% endspaceless %}
{% load i18n %}
{% spaceless %}
<section class="open-response-assessment-block"
data-item-view-enabled="{{ ora_item_view_enabled }}"
data-rendered=0>
<div class="open-response-assessment-msg">{% trans "Please wait" %}</div>
<div class="open-response-assessment-content">
<div class="open-response-assessment-summary"></div>
<hr/>
<div class="open-response-assessment-main-table"></div>
</div>
<div class="open-response-assessment-item">
<div class="open-response-assessment-item-breadcrumbs"></div>
<div class="open-response-assessment-item-block"></div>
</div>
</section>
<script type="text/template" id="open-response-assessment-items">{{ ora_items |safe }}</script>
<script type="text/template" id="open-response-assessment-summary-tpl">
{% include "openassessmentblock/instructor_dashboard/open-response-assessment-summary.underscore" %}
</script>
{% endspaceless %}
<table class="backgrid open-response-assessment-summary">
<tbody>
<tr>
<% for (var i = 0; i < oraSummary.length; i++) {
var s = oraSummary[i];
%>
<td class="string-cell renderable <%- s.class %>">
<div class="ora-summary-title"><%- s.title %></div>
<div class="ora-summary-value"><%- s.value %></div>
</td>
<% } %>
</tr>
</tbody>
</table>
"""
The mixin with handlers for the course ora blocks listing view.
"""
import json
from webob import Response
from xblock.core import XBlock
from openassessment.data import OraAggregateData
from openassessment.xblock.staff_area_mixin import require_course_staff
class CourseItemsListingMixin(object):
"""
The mixin with handlers for the course ora blocks listing view.
"""
@XBlock.handler
@require_course_staff('STAFF_AREA')
def get_ora2_responses(self, request, suffix=''): # pylint: disable=unused-argument
"""
Get information about all ora2 blocks in the course with response count for each step.
"""
responses = OraAggregateData.collect_ora2_responses(unicode(self.course_id))
return Response(json.dumps(responses), content_type='application/json')
...@@ -37,6 +37,7 @@ from openassessment.xblock.student_training_mixin import StudentTrainingMixin ...@@ -37,6 +37,7 @@ from openassessment.xblock.student_training_mixin import StudentTrainingMixin
from openassessment.xblock.validation import validator from openassessment.xblock.validation import validator
from openassessment.xblock.resolve_dates import resolve_dates, parse_date_value, DISTANT_PAST, DISTANT_FUTURE from openassessment.xblock.resolve_dates import resolve_dates, parse_date_value, DISTANT_PAST, DISTANT_FUTURE
from openassessment.xblock.data_conversion import create_prompts_list, create_rubric_dict, update_assessments_format from openassessment.xblock.data_conversion import create_prompts_list, create_rubric_dict, update_assessments_format
from openassessment.xblock.course_items_listing_mixin import CourseItemsListingMixin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -97,21 +98,20 @@ def load(path): ...@@ -97,21 +98,20 @@ def load(path):
@XBlock.needs("i18n") @XBlock.needs("i18n")
@XBlock.needs("user") @XBlock.needs("user")
class OpenAssessmentBlock( class OpenAssessmentBlock(MessageMixin,
MessageMixin, SubmissionMixin,
SubmissionMixin, PeerAssessmentMixin,
PeerAssessmentMixin, SelfAssessmentMixin,
SelfAssessmentMixin, StaffAssessmentMixin,
StaffAssessmentMixin, StudioMixin,
StudioMixin, GradeMixin,
GradeMixin, LeaderboardMixin,
LeaderboardMixin, StaffAreaMixin,
StaffAreaMixin, WorkflowMixin,
WorkflowMixin, StudentTrainingMixin,
StudentTrainingMixin, LmsCompatibilityMixin,
LmsCompatibilityMixin, CourseItemsListingMixin,
XBlock, XBlock):
):
"""Displays a prompt and provides an area where students can compose a response.""" """Displays a prompt and provides an area where students can compose a response."""
public_dir = 'static' public_dir = 'static'
...@@ -364,12 +364,87 @@ class OpenAssessmentBlock( ...@@ -364,12 +364,87 @@ class OpenAssessmentBlock(
context = Context(context_dict) context = Context(context_dict)
fragment = Fragment(template.render(context)) fragment = Fragment(template.render(context))
return self._update_and_return_fragment(fragment, 'OpenAssessmentBlock')
def ora_blocks_listing_view(self, context=None):
"""This view is used in the Open Response Assessment tab in the LMS Instructor Dashboard
to display all available course ORA blocks.
Args:
context: contains two items:
"ora_items" - all course items with names and parents, example:
[{"parent_name": "Vertical name",
"name": "ORA Display Name",
"url_grade_available_responses": "/grade_available_responses_view",
"staff_assessment": false,
"parent_id": "vertical_block_id",
"url_base": "/student_view",
"id": "openassessment_block_id"
}, ...]
"ora_item_view_enabled" - enabled LMS API endpoint to serve XBlock view or not
Returns:
(Fragment): The HTML Fragment for this XBlock.
"""
ora_items = context.get('ora_items', []) if context else []
ora_item_view_enabled = context.get('ora_item_view_enabled', False) if context else False
context_dict = {
"ora_items": json.dumps(ora_items),
"ora_item_view_enabled": 1 if ora_item_view_enabled else 0
}
template = get_template('openassessmentblock/instructor_dashboard/oa_listing.html')
context = Context(context_dict)
fragment = Fragment(template.render(context))
return self._update_and_return_fragment(fragment, 'CourseOpenResponsesListingBlock')
def grade_available_responses_view(self, context=None):
"""Grade Available Responses view.
Auxiliary view which displays the staff grading area
(used in the Open Response Assessment tab in the Instructor Dashboard of LMS)
Args:
context: Not used for this view.
Returns:
(Fragment): The HTML Fragment for this XBlock.
"""
student_item = self.get_student_item_dict()
staff_assessment_required = "staff-assessment" in self.assessment_steps
context_dict = {
"title": self.title,
'staff_assessment_required': staff_assessment_required
}
if staff_assessment_required:
context_dict.update(
self.get_staff_assessment_statistics_context(student_item["course_id"], student_item["item_id"])
)
template = get_template('openassessmentblock/instructor_dashboard/oa_grade_available_responses.html')
context = Context(context_dict)
fragment = Fragment(template.render(context))
return self._update_and_return_fragment(fragment, 'StaffAssessmentBlock')
def _update_and_return_fragment(self, fragment, initialize_js_func):
"""
Auxiliary function to return updated fragment
"""
i18n_service = self.runtime.service(self, 'i18n') i18n_service = self.runtime.service(self, 'i18n')
if hasattr(i18n_service, 'get_language_bidi') and i18n_service.get_language_bidi(): if hasattr(i18n_service, 'get_language_bidi') and i18n_service.get_language_bidi():
css_url = "static/css/openassessment-rtl.css" css_url = "static/css/openassessment-rtl.css"
else: else:
css_url = "static/css/openassessment-ltr.css" css_url = "static/css/openassessment-ltr.css"
# backgrid.js v0.3.7
fragment.add_css_url(self.runtime.local_resource_url(self, "static/css/lib/backgrid.min.css"))
self.add_javascript_files(fragment, "static/js/lib/backgrid.min.js")
if settings.DEBUG: if settings.DEBUG:
fragment.add_css_url(self.runtime.local_resource_url(self, css_url)) fragment.add_css_url(self.runtime.local_resource_url(self, css_url))
self.add_javascript_files(fragment, "static/js/src/oa_shared.js") self.add_javascript_files(fragment, "static/js/src/oa_shared.js")
...@@ -385,7 +460,7 @@ class OpenAssessmentBlock( ...@@ -385,7 +460,7 @@ class OpenAssessmentBlock(
"FILE_EXT_BLACK_LIST": self.FILE_EXT_BLACK_LIST, "FILE_EXT_BLACK_LIST": self.FILE_EXT_BLACK_LIST,
"FILE_TYPE_WHITE_LIST": self.white_listed_file_types, "FILE_TYPE_WHITE_LIST": self.white_listed_file_types,
} }
fragment.initialize_js('OpenAssessmentBlock', js_context_dict) fragment.initialize_js(initialize_js_func, js_context_dict)
return fragment return fragment
@property @property
......
.backgrid-container{position:relative;display:block;width:100%;height:465px;padding:0;overflow:auto;border:0}.backgrid{width:100%;max-width:100%;background-color:transparent;border-collapse:collapse;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.backgrid thead th,.backgrid.backgrid-striped tbody tr:nth-child(even){background-color:#f9f9f9}.backgrid td,.backgrid th{display:none;height:20px;max-width:250px;padding:4px 5px;overflow:hidden;line-height:20px;text-align:left;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;border-bottom:1px solid #DDD}.backgrid td.renderable,.backgrid th.renderable{display:table-cell}.backgrid th{font-weight:700;text-align:center}.backgrid th.sortable a{text-decoration:none;white-space:nowrap;cursor:pointer}.backgrid thead th{vertical-align:bottom}.backgrid thead th a{display:block}.backgrid tbody tr.empty{font-style:italic;color:gray}.backgrid tbody tr.empty td{display:table-cell;text-align:center}.backgrid td.editor{padding:0}.backgrid tbody tr:nth-child(odd) td.editor,.backgrid td.editor{background-color:rgba(82,168,236,.1);outline:rgba(82,168,236,.8) solid 1px;outline-offset:-1px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition-duration:.2s;-moz-transition-duration:.2s;-o-transition-duration:.2s;transition-duration:.2s;-webkit-transition-property:width,outline,background-color;-moz-transition-property:width,outline,background-color;-o-transition-property:width,outline,background-color;transition-property:width,outline,background-color;-webkit-transition-timing-function:ease-in-out;-moz-transition-timing-function:ease-in-out;-o-transition-timing-function:ease-in-out;transition-timing-function:ease-in-out}.backgrid td.editor input[type=text]{display:block;width:100%;height:100%;padding:0 5px;margin:0;background-color:transparent;border:0;outline:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none}.backgrid td.editor input[type=text]::-ms-clear{display:none}.backgrid tbody tr:nth-child(odd) td.error,.backgrid td.error{background-color:rgba(255,210,77,.1);outline:#ffd24d solid 1px}.backgrid td.editor :focus,.backgrid th.editor :focus{outline:0}.backgrid .sort-caret{display:inline-block;width:0;height:0;margin-left:.3em;border:0;content:""}.backgrid .ascending .sort-caret,.backgrid .descending .sort-caret{border-right:4px solid transparent;border-left:4px solid transparent}.backgrid .ascending .sort-caret{vertical-align:baseline;border-top:none;border-bottom:4px solid #000}.backgrid .descending .sort-caret{vertical-align:super;border-top:4px solid #000;border-bottom:none}.backgrid .email-cell,.backgrid .email-cell.editor input[type=text],.backgrid .string-cell,.backgrid .string-cell.editor input[type=text],.backgrid .uri-cell,.backgrid .uri-cell.editor input[type=text]{text-align:left}.backgrid .date-cell,.backgrid .date-cell.editor input[type=text],.backgrid .datetime-cell,.backgrid .datetime-cell.editor input[type=text],.backgrid .integer-cell,.backgrid .integer-cell.editor input[type=text],.backgrid .number-cell,.backgrid .number-cell.editor input[type=text],.backgrid .percent-cell,.backgrid .percent-cell.editor input[type=text],.backgrid .time-cell,.backgrid .time-cell.editor input[type=text]{text-align:right}.backgrid .boolean-cell,.backgrid .boolean-cell.editor input[type=checkbox],.backgrid .select-cell{text-align:center}.backgrid .select-cell.editor{padding:0}.backgrid .select-cell.editor select{display:block;width:100%;height:28px;padding:4px 5px;margin:0;line-height:28px;vertical-align:middle;background-color:#fff;border:0;outline:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.backgrid .select-cell.editor select[multiple]{height:auto}.backgrid .select-cell.editor :focus{border:0;outline:0}.backgrid .select-cell.editor optgroup::-moz-focus-inner,.backgrid .select-cell.editor optgroup::-o-focus-inner,.backgrid .select-cell.editor option::-moz-focus-inner,.backgrid .select-cell.editor option::-o-focus-inner,.backgrid .select-cell.editor select::-moz-focus-inner,.backgrid .select-cell.editor select::-o-focus-inner{border:0}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -1213,5 +1213,23 @@ ...@@ -1213,5 +1213,23 @@
"submission_due": "2020-01-01T00:00" "submission_due": "2020-01-01T00:00"
}, },
"output": "oa_response_date.html" "output": "oa_response_date.html"
},
{
"template": "openassessmentblock/instructor_dashboard/oa_grade_available_responses.html",
"context": {
"title": "Test ABC",
"staff_assessment_required": true,
"staff_assessment_ungraded": 9,
"staff_assessment_in_progress": 2
},
"output": "oa_grade_available_responses_separate_view.html"
},
{
"template": "openassessmentblock/instructor_dashboard/oa_listing.html",
"context": {
"ora_item_view_enabled": "1",
"ora_items": ""
},
"output": "oa_listing_view.html"
} }
] ]
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -103,6 +103,13 @@ describe('OpenAssessment.StaffAreaView', function() { ...@@ -103,6 +103,13 @@ describe('OpenAssessment.StaffAreaView', function() {
return view; return view;
}; };
var createGradeAvailableResponsesView = function() {
var assessmentElement = $('.openassessment').get(0);
var view = new OpenAssessment.BaseView(runtime, assessmentElement, server, {});
view.staffAreaView.installHandlers();
return view;
};
/** /**
* Initialize the staff area view, then check whether it makes * Initialize the staff area view, then check whether it makes
* an AJAX call to populate itself. * an AJAX call to populate itself.
...@@ -763,4 +770,51 @@ describe('OpenAssessment.StaffAreaView', function() { ...@@ -763,4 +770,51 @@ describe('OpenAssessment.StaffAreaView', function() {
expect(staffArea.baseView.unsavedWarningEnabled()).toBe(false); expect(staffArea.baseView.unsavedWarningEnabled()).toBe(false);
}); });
}); });
describe('Grade Available Responses as the separate view', function() {
var staffAreaTab = "staff-grading";
var gradingType = "full-grade";
beforeEach(function() {
loadFixtures('oa_grade_available_responses_separate_view.html');
});
it('exists without any additional buttons', function() {
var view = createGradeAvailableResponsesView(),
staffArea = $('.openassessment__staff-area', view.element),
staffGradingButton = $('.button-staff-grading', view.element),
problemHeader = $('.problem__header', view.element),
gradeValue = $('.staff__grade__value', view.element);
expect(staffArea.length).toBe(1);
expect(staffGradingButton.length).toBe(0);
expect(problemHeader.text()).toBe('Test ABC');
expect(gradeValue.text().trim()).toBe("9 Available and 2 Checked Out");
});
it('can submit a staff grade', function() {
var view = createGradeAvailableResponsesView(),
$assessment, $staffGradeButton;
$staffGradeButton = $('.staff__grade__show-form', view.element);
expect($staffGradeButton).toHaveAttr('aria-expanded', 'false');
showInstructorAssessmentForm(view);
expect($staffGradeButton).toHaveAttr('aria-expanded', 'true');
$assessment = getAssessment(view, staffAreaTab);
// Verify that the submission is shown
expect($('.staff-assessment__display__title', view.element).text().trim()).toBe(
'Response for: mock_user'
);
// Fill in and submit the assessment
fillAssessment($assessment, gradingType);
server.staffGradeFormTemplate = 'oa_staff_grade_learners_assessment_2.html';
submitAssessment(view.staffAreaView, staffAreaTab);
verifyAssessType(view.staffAreaView, 'full-grade');
// Verify that the assessment form has been removed
expect($('.staff__grade__form', view.element).html().trim()).toBe('');
expect($staffGradeButton).toHaveAttr('aria-expanded', 'false');
verifyFocused($staffGradeButton[0]);
});
});
}); });
...@@ -253,7 +253,7 @@ describe("OpenAssessment.Server", function() { ...@@ -253,7 +253,7 @@ describe("OpenAssessment.Server", function() {
type: "POST", type: "POST",
data: JSON.stringify({ data: JSON.stringify({
feedback_text: "test feedback", feedback_text: "test feedback",
feedback_options: options, feedback_options: options
}), }),
contentType : jsonContentType contentType : jsonContentType
}); });
......
...@@ -384,5 +384,22 @@ function OpenAssessmentBlock(runtime, element, data) { ...@@ -384,5 +384,22 @@ function OpenAssessmentBlock(runtime, element, data) {
var server = new OpenAssessment.Server(runtime, element); var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.BaseView(runtime, element, server, data); var view = new OpenAssessment.BaseView(runtime, element, server, data);
view.load(); view.load();
}
/* XBlock JavaScript entry point for OpenAssessmentXBlock. */
/* jshint unused:false */
function CourseOpenResponsesListingBlock(runtime, element, data) {
var view = new OpenAssessment.CourseItemsListingView(runtime, element);
view.refreshGrids();
}
/* XBlock JavaScript entry point for OpenAssessmentXBlock. */
/* jshint unused:false */
function StaffAssessmentBlock(runtime, element, data) {
/**
Render auxiliary view which displays the staff grading area
**/
var server = new OpenAssessment.Server(runtime, element);
var view = new OpenAssessment.BaseView(runtime, element, server, data);
view.staffAreaView.installHandlers();
} }
(function(OpenAssessment) {
'use strict';
OpenAssessment.CourseItemsListingView = function(runtime, element) {
var self = this;
var $section = $(element);
var block = $section.find('.open-response-assessment-block');
var itemViewEnabled = (parseInt(block.data('item-view-enabled')) === 1) && XBlock;
this.$section = $section;
this.runtime = runtime;
this.oraData = $.parseJSON($("#open-response-assessment-items").text());
$section.find(".open-response-assessment-content").hide();
$section.find('.open-response-assessment-item').hide();
$section.find('.open-response-assessment-msg').show();
var AssessmentCell = Backgrid.UriCell.extend({
staff: false,
render: function() {
this.$el.empty();
var url = this.model.get(this.staff ? 'url_grade_available_responses' : 'url_base');
var rawValue = this.model.get(this.column.get("name"));
var staffAssessment = this.model.get('staff_assessment');
var formattedValue = this.formatter.fromRaw(rawValue, this.model);
var link = null;
if (itemViewEnabled && (!this.staff || (this.staff && staffAssessment))) {
link = $("<a>", {
text: formattedValue,
title: this.title || formattedValue
});
this.$el.append(link);
link.on("click", $.proxy(self, "displayOraBlock", url));
} else {
this.$el.append(formattedValue);
}
this.delegateEvents();
return this;
}
});
var StaffCell = AssessmentCell.extend({
staff: true
});
this._columns = [
{
name: 'parent_name', label: gettext("Parent Section"), label_summary: gettext("Parent Sections"),
cell: "string", num: false, editable: false
},
{
name: 'name', label: gettext("Assessment"), label_summary: gettext("Assessments"),
cell: AssessmentCell, num: false, editable: false
},
{
name: 'total', label: gettext("Total Responses"), label_summary: gettext("Total Responses"),
cell: "string", num: true, editable: false
},
{
name: 'training', label: gettext("Training"), label_summary: gettext("Training"),
cell: "string", num: true, editable: false
},
{
name: 'peer', label: gettext("Peer"), label_summary: gettext("Peer"),
cell: "string", num: true, editable: false
},
{
name: 'self', label: gettext("Self"), label_summary: gettext("Self"),
cell: "string", num: true, editable: false
},
{
name: 'staff', label: gettext("Staff"), label_summary: gettext("Staff"),
cell: StaffCell, num: true, editable: false
},
{
name: 'done',
label: gettext("Final Grade Received"),
label_summary: gettext("Final Grade Received"),
cell: "string",
num: true,
editable: false
}
];
};
OpenAssessment.CourseItemsListingView.prototype.refreshGrids = function(force) {
force = force || false;
var self = this;
var $section = this.$section;
var block = $section.find('.open-response-assessment-block');
var dataUrl = this.runtime.handlerUrl($section, 'get_ora2_responses');
var dataRendered = parseInt(block.data('rendered'));
if (!dataRendered || force) {
return $.Deferred(
function(defer) {
$.ajax({
type: 'GET',
dataType: 'json',
url: dataUrl
}).done(function(data) {
self.renderGrids(data);
defer.resolve();
}).fail(function(data, textStatus) {
$section.find('.open-response-assessment-msg')
.text(gettext('List of Open Assessments is unavailable'));
defer.rejectWith(this, [textStatus]);
});
}
).promise();
}
};
OpenAssessment.CourseItemsListingView.prototype.renderGrids = function(data) {
var self = this;
var $section = this.$section;
var block = $section.find('.open-response-assessment-block');
var oraSteps = ['training', 'peer', 'self', 'staff', 'done'];
$.each(self.oraData, function(i, oraItem) {
var total = 0;
var itemId = oraItem.id;
$.each(oraSteps, function(j, step) {
self.oraData[i][step] = 0;
});
if (itemId in data) {
_.extend(self.oraData[i], data[itemId]);
}
$.each(oraSteps, function(j, step) {
total += self.oraData[i][step];
});
self.oraData[i].total = total;
});
block.data('rendered', 1);
$section.find('.open-response-assessment-msg').hide();
self.showSummaryGrid(self.oraData);
self.showOpenResponsesGrid(self.oraData);
};
OpenAssessment.CourseItemsListingView.prototype.showSummaryGrid = function(data) {
var $section = this.$section;
var summaryData = [];
var summaryDataMap = {};
$section.find(".open-response-assessment-summary").empty();
$.each(this._columns, function(index, v) {
summaryData.push({
title: v.label_summary,
value: 0,
num: v.num,
class: v.name
});
summaryDataMap[v.name] = index;
});
$.each(data, function(index, obj) {
$.each(obj, function(key, value) {
var idx = 0;
if (key in summaryDataMap) {
idx = summaryDataMap[key];
if (summaryData[idx].num) {
summaryData[idx].value += value;
} else {
summaryData[idx].value += 1;
}
}
});
});
var templateData = _.template($('#open-response-assessment-summary-tpl').text());
$section.find(".open-response-assessment-summary").append(templateData({
oraSummary: summaryData
}));
};
OpenAssessment.CourseItemsListingView.prototype.showOpenResponsesGrid = function(data) {
var $section = this.$section;
$section.find('.open-response-assessment-content').show();
var collection = new Backbone.Collection(data);
$section.find(".open-response-assessment-main-table").empty();
var grid = new Backgrid.Grid({
columns: this._columns,
collection: collection
});
$section.find(".open-response-assessment-main-table").append(grid.render().el);
};
OpenAssessment.CourseItemsListingView.prototype.displayOraBlock = function(url) {
var $section = this.$section;
var self = this;
$section.find(".open-response-assessment-content").hide();
$section.find('.open-response-assessment-msg').text(gettext('Please wait')).show();
return $.Deferred(
function(defer) {
$.ajax({
type: 'GET',
dataType: 'json',
url: url
}).done(function(data) {
var el = $section.find('.open-response-assessment-item');
var block = el.find('.open-response-assessment-item-block');
$section.find('.open-response-assessment-msg').hide();
el.show();
self.renderBreadcrumbs();
block.html(data.html);
XBlock.initializeBlock($(block).find('.xblock')[0]);
defer.resolve();
}).fail(function(data, textStatus) {
$section.find('.open-response-assessment-item').show();
$section.find('.open-response-assessment-msg')
.text(gettext('Block view is unavailable'));
self.renderBreadcrumbs();
defer.rejectWith(this, [textStatus]);
});
}
).promise();
};
OpenAssessment.CourseItemsListingView.prototype.renderBreadcrumbs = function() {
var $section = this.$section;
var breadcrumbs = $section.find(".open-response-assessment-item-breadcrumbs");
var text = gettext('Back to Full List');
var fullListItem = $("<a>", {
html: '&larr;&nbsp;' + text,
title: text
});
breadcrumbs.append(fullListItem);
fullListItem.on("click", $.proxy(this, "backToOpenResponsesGrid"));
};
OpenAssessment.CourseItemsListingView.prototype.backToOpenResponsesGrid = function() {
var $section = this.$section;
$section.find(".open-response-assessment-item-breadcrumbs").empty();
$section.find(".open-response-assessment-item-block").empty();
$section.find('.open-response-assessment-item').hide();
$section.find('.open-response-assessment-msg').text(gettext('Please wait')).show();
this.refreshGrids(true);
};
})(OpenAssessment);
...@@ -24,7 +24,7 @@ OpenAssessment.FileUploader = function() { ...@@ -24,7 +24,7 @@ OpenAssessment.FileUploader = function() {
data: file, data: file,
async: false, async: false,
processData: false, processData: false,
contentType: file.type, contentType: file.type
}).done( }).done(
function() { function() {
// Log an analytics event // Log an analytics event
......
...@@ -81,6 +81,7 @@ ...@@ -81,6 +81,7 @@
@import 'oa/elements/layout'; // applied layouts and deliberate class-based breakpoints @import 'oa/elements/layout'; // applied layouts and deliberate class-based breakpoints
@import 'oa/elements/staff'; // open assessment staff-centric UI @import 'oa/elements/staff'; // open assessment staff-centric UI
@import 'oa/views/oa-base'; // open assessment base view @import 'oa/views/oa-base'; // open assessment base view
@import 'oa/views/oa-course-items-listing'; // open response assessment tab for instructor dashboard
// openassessment: contextual // openassessment: contextual
@import 'oa/contexts/ie'; // open assessment-specific Internet Explorer-specific styling @import 'oa/contexts/ie'; // open assessment-specific Internet Explorer-specific styling
......
.open-response-assessment-block {
table {
table-layout: fixed;
}
.open-response-assessment-msg {
margin-bottom: 20px;
}
.open-response-assessment-content,
.open-response-assessment-item
{
display: none;
}
.open-response-assessment-summary {
td {
border: none;
}
}
.open-response-assessment-item-breadcrumbs {
padding-bottom: 20px;
a {
cursor: pointer;
}
}
.open-response-assessment-main-table {
th {
background-color: $white;
text-align: left;
a {
color: $black !important;
}
}
td {
a {
cursor: pointer;
}
}
tbody tr:nth-child(odd) {
background-color: #f9f9f9;
}
}
.ora-summary-title {
color: #0079bc;
font-weight: bold;
text-transform: uppercase;
font-size: 0.8em;
}
.ora-summary-value {
font-size: 2.2em;
padding-top: 3px;
}
.backgrid {
.peer, .self, .staff {
width: 65px;
}
.training {
width: 90px;
}
.total {
width: 150px;
}
.done {
width: 190px;
}
}
}
# -*- coding: utf-8 -*-
"""
Tests for course items listing handlers.
"""
import json
from mock import patch
from .base import scenario, XBlockHandlerTestCase, SubmitAssessmentsMixin
class TestCourseItemsListingHandlers(XBlockHandlerTestCase, SubmitAssessmentsMixin):
"""
Test for course items listing handlers.
"""
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_non_staff_access(self, xblock):
"""
Test non-staff access for endpoint that return items statistics
"""
response = self.request(xblock, 'get_ora2_responses', json.dumps({}),
request_method='GET')
self.assertIn('You do not have permission', response)
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_staff_access(self, xblock):
"""
Test staff access for endpoint that return items statistics
"""
self.set_staff_access(xblock)
xblock.xmodule_runtime.course_id = 'test_course'
return_data = [{'id': 'test_1', 'title': 'title_1'},
{'id': 'test_2', 'title': 'title_2'}]
with patch("openassessment.data.OraAggregateData.collect_ora2_responses", return_value=return_data):
response = self.request(xblock, 'get_ora2_responses', json.dumps({}),
request_method='GET', response_format='json')
self.assertEqual(response, return_data)
...@@ -2,12 +2,15 @@ ...@@ -2,12 +2,15 @@
Tests the Open Assessment XBlock functionality. Tests the Open Assessment XBlock functionality.
""" """
import ddt import ddt
import json
from collections import namedtuple from collections import namedtuple
import datetime as dt import datetime as dt
from freezegun import freeze_time from freezegun import freeze_time
import pytz import pytz
from mock import Mock, patch, MagicMock, PropertyMock from mock import Mock, patch, MagicMock, PropertyMock
from lxml import etree
from StringIO import StringIO
from openassessment.xblock import openassessmentblock from openassessment.xblock import openassessmentblock
from openassessment.xblock.resolve_dates import DISTANT_PAST, DISTANT_FUTURE from openassessment.xblock.resolve_dates import DISTANT_PAST, DISTANT_FUTURE
...@@ -59,6 +62,85 @@ class TestOpenAssessment(XBlockHandlerTestCase): ...@@ -59,6 +62,85 @@ class TestOpenAssessment(XBlockHandlerTestCase):
self.assertIsNotNone(grade_response) self.assertIsNotNone(grade_response)
self.assertIn("step--grade", grade_response.body) self.assertIn("step--grade", grade_response.body)
def _staff_assessment_view_helper(self, xblock):
"""
Helper for "staff_assessment_view" tests
"""
xblock_fragment = self.runtime.render(xblock, "grade_available_responses_view")
body_html = xblock_fragment.body_html()
self.assertIn("StaffAssessmentBlock", body_html)
self.assertIn("openassessment__title", body_html)
return body_html
@scenario('data/staff_grade_scenario.xml')
def test_staff_assessment_view(self, xblock):
"""OA XBlock returns some HTML for case if Staff Assessment is configured.
View basic test for verifying auxiliary view which displays the staff grading area.
"""
body_html = self._staff_assessment_view_helper(xblock)
self.assertIn("openassessment__staff-area", body_html)
self.assertIn("ui-staff__content", body_html)
self.assertNotIn("openassessment__staff-area-unavailable", body_html)
@scenario('data/basic_scenario.xml')
def test_staff_assessment_view_staff_assessment_not_configured(self, xblock):
"""OA XBlock returns some HTML for case if Staff Assessment is not configured.
View basic test for verifying auxiliary view which displays the staff grading area.
"""
body_html = self._staff_assessment_view_helper(xblock)
self.assertNotIn("ui-staff__content", body_html)
self.assertIn("openassessment__staff-area-unavailable", body_html)
@scenario('data/basic_scenario.xml')
def test_ora_blocks_listing_view(self, xblock):
"""
Test view for listing all courses OA blocks.
"""
xblock_fragment = self.runtime.render(xblock, "ora_blocks_listing_view")
body_html = xblock_fragment.body_html()
self.assertIn("CourseOpenResponsesListingBlock", body_html)
parser = etree.HTMLParser()
tree = etree.parse(StringIO(body_html), parser)
xpath_query_to_get_main_section = "//section[contains(@class, 'open-response-assessment-block')]"
xpath_query_to_get_course_items = "//script[contains(@id, 'open-response-assessment-items')]"
sections = tree.xpath(xpath_query_to_get_main_section)
self.assertEqual(len(sections), 1)
self.assertEquals(sections[0].get('data-item-view-enabled'), '0')
scripts = tree.xpath(xpath_query_to_get_course_items)
self.assertEqual(len(scripts), 1)
self.assertEqual(scripts[0].text, '[]')
defined_ora_items = [{'id': 'test-id1', 'val': 'test-val1'},
{'id': 'test-id2', 'val': 'test-val2'}]
xblock_fragment = self.runtime.render(xblock, "ora_blocks_listing_view", context={
'ora_items': defined_ora_items,
'ora_item_view_enabled': True
})
body_html = xblock_fragment.body_html()
parser = etree.HTMLParser()
tree = etree.parse(StringIO(body_html), parser)
sections = tree.xpath(xpath_query_to_get_main_section)
self.assertEqual(len(sections), 1)
self.assertEquals(sections[0].get('data-item-view-enabled'), '1')
scripts = tree.xpath(xpath_query_to_get_course_items)
self.assertEqual(len(scripts), 1)
items = json.loads(scripts[0].text)
self.assertEqual(items, defined_ora_items)
@scenario('data/empty_prompt.xml') @scenario('data/empty_prompt.xml')
def test_prompt_intentionally_empty(self, xblock): def test_prompt_intentionally_empty(self, xblock):
# Verify that prompts intentionally left empty don't create DOM elements # Verify that prompts intentionally left empty don't create DOM elements
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
"version": "0.2.0", "version": "0.2.0",
"repository": "https://github.com/edx/edx-ora2.git", "repository": "https://github.com/edx/edx-ora2.git",
"dependencies": { "dependencies": {
"backbone": "~1.2.3",
"edx-ui-toolkit": "1.5.1", "edx-ui-toolkit": "1.5.1",
"moment": "^2.15.1", "moment": "^2.15.1",
"moment-timezone": "~0.5.5", "moment-timezone": "~0.5.5",
......
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