Commit f083c9fd by cahrens

Staff tool UI for full staff grading.

TNL-3493
parent 5cae4ead
......@@ -265,7 +265,7 @@ def get_submission_to_assess(course_id, item_id, scorer_id):
'attempt_number': 1,
'submitted_at': datetime.datetime(2014, 1, 29, 23, 14, 52, 649284, tzinfo=<UTC>),
'created_at': datetime.datetime(2014, 1, 29, 17, 14, 52, 668850, tzinfo=<UTC>),
'answer': u'The answer is 42.'
'answer': { ... }
}
"""
......
......@@ -167,6 +167,8 @@
</div>
</div>
<!-- Conditional not really required because button will be hidden. -->
{% if staff_assessment_required %}
<div class="openassessment__staff-grading wrapper--staff-grading wrapper--ui-staff is--hidden">
<div class="staff-grading ui-staff">
<h2 class="staff-grading__title ui-staff__title">
......@@ -175,8 +177,10 @@
</h2>
<div class="staff-info__content ui-staff__content">
{% include "openassessmentblock/staff_area/oa_staff_grade_learners.html" with staff_assessment_ungraded=staff_assessment_ungraded %}
</div>
</div>
</div>
{% endif %}
</div>
{% load i18n %}
<!-- staff-info__student is necessary to get proper styling for the staff assessment form -->
<div class="staff__grade__control ui-toggle-visibility is--collapsed staff-info__student">
<header class="staff__grade__header ui-toggle-visibility__control">
<h3 class="staff__grade__title">
<span class="wrapper--copy">
<span class="staff__grade__label">{% trans "Instructor Assessment" %}</span>
</span>
</h3>
{% block title %}
<span class="staff__grade__status">
<span class="staff__grade__value">
<span class="copy">
{% blocktrans with ungraded=staff_assessment_ungraded|stringformat:"s" in_progress=staff_assessment_in_progress|stringformat:"s" %}
{{ ungraded }} Available and {{ in_progress }} Checked Out
{% endblocktrans %}
</span>
</span>
</span>
{% endblock %}
</header>
<div class="staff__grade__content ui-toggle-visibility__content">
<div class="wrapper--input">
<div class="staff__grade__form"></div>
</div>
</div>
</div>
\ No newline at end of file
{% load i18n %}
{% spaceless %}
{% block body %}
<div class="staff__grade__form ui-toggle-visibility__content" data-submission-uuid="{{ submission.uuid }}">
<div class="wrapper--staff-assessment">
<div class="step__instruction">
<p>{% trans "Give this learner a grade using the problem's rubric." %}</p>
</div>
<div class="step__content">
<article class="staff-assessment">
<div class="staff-assessment__display">
<header class="staff-assessment__display__header">
<h4 class="staff-assessment__display__title">
{% blocktrans %}
Response for: {{ student_username }}
{% endblocktrans %}
</h4>
</header>
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label="The learner's response to the question above:" %}
{% include "openassessmentblock/oa_uploaded_file.html" with file_upload_type=file_upload_type file_url=staff_file_url header="Associated File" class_prefix="staff-assessment" show_warning="true" %}
</div>
<form class="staff-assessment__assessment" method="post">
{% include "openassessmentblock/oa_rubric.html" with rubric_type="staff" rubric_feedback_prompt="(Optional) What aspects of this response stood out to you? What did it do well? How could it improve?" rubric_feedback_default_text="I noticed that this response..." %}
</form>
</article>
</div>
<div class="step__actions">
<div class="message message--inline message--error message--error-server">
<h4 class="message__title">{% trans "We could not submit your assessment" %}</h4>
<div class="message__content"></div>
</div>
<ul class="list list--actions">
<li class="list--actions__item submit_assessment--action">
<button type="submit" class="action action--submit is--disabled">
<span class="copy">{% trans "Submit Assessment" %}</span>
</button>
</li>
<li class="list--actions__item submit_assessment--action">
<button type="submit" class="action action--submit is--disabled continue_grading--action">
<span class="copy">{% trans "Submit Assessment and Grade Another Learner" %}</span>
</button>
</li>
</ul>
<div class="staff-grade-error"></div>
</div>
</div>
</div>
{% endblock %}
{% endspaceless %}
......@@ -24,6 +24,7 @@ from openassessment.assessment.api import self as self_api
from openassessment.assessment.api import ai as ai_api
from openassessment.fileupload import api as file_api
from openassessment.workflow import api as workflow_api
from openassessment.assessment.api import staff as staff_api
from openassessment.fileupload import exceptions as file_exceptions
......@@ -164,7 +165,14 @@ class StaffAreaMixin(object):
})
# Include whether or not staff grading step is enabled.
context['staff_assessment_required'] = "staff-assessment" in self.assessment_steps
staff_assessment_required = "staff-assessment" in self.assessment_steps
context['staff_assessment_required'] = staff_assessment_required
if staff_assessment_required:
grading_stats = staff_api.get_staff_grading_statistics(
student_item["course_id"], student_item["item_id"]
)
context['staff_assessment_ungraded'] = grading_stats['ungraded']
context['staff_assessment_in_progress'] = grading_stats['in-progress']
return path, context
......@@ -226,7 +234,74 @@ class StaffAreaMixin(object):
return self.render_assessment(path, context)
except PeerAssessmentInternalError:
return self.render_error(self._(u"Error finding assessment workflow cancellation."))
return self.render_error(self._(u"Error finding assessment workflow cancellation.")) # TODO: this error is too specific
@XBlock.handler
@require_course_staff("STUDENT_INFO") # TODO: should this be a different "permission"?
def render_staff_grade_form(self, data, suffix=''): # pylint: disable=W0613
"""
Renders all relative information for a specific student's workflow. TODO update
Given a student's username, we can render a staff-only section of the page
with submissions and assessments specific to the student.
Must be course staff to render this view.
"""
try:
student_item_dict = self.get_student_item_dict()
course_id = student_item_dict.get('course_id')
item_id = student_item_dict.get('item_id')
staff_id = student_item_dict['student_id']
submission_to_assess = staff_api.get_submission_to_assess(course_id, item_id, staff_id)
if submission_to_assess is not None:
submission = submission_api.get_submission_and_student(submission_to_assess['uuid'])
if submission:
anonymous_student_id = submission['student_item']['student_id']
submission_context = self.get_student_submission_context(
self.get_username(anonymous_student_id), submission
)
path = 'openassessmentblock/staff_area/oa_staff_grade_learners_assessment.html'
return self.render_assessment(path, submission_context)
else:
return self.render_error(self._(u"No more assessments can be graded at this time."))
except PeerAssessmentInternalError:
return self.render_error(self._(u"Error finding assessment workflow cancellation.")) # TODO Update!
def get_student_submission_context(self, student_username, submission):
"""
TODO: update!
Get the proper path and context for rendering the student info
section of the staff area.
Args:
student_username (unicode): The username of the student to report.
"""
if submission:
if 'file_key' in submission.get('answer', {}):
file_key = submission['answer']['file_key']
try:
submission['file_url'] = file_api.get_download_url(file_key)
except file_exceptions.FileUploadError:
# Log the error, but do not prevent the rest of the student info
# from being displayed.
msg = (
u"Could not retrieve image URL for staff debug page. "
u"The learner username is '{student_username}', and the file key is {file_key}"
).format(student_username=student_username, file_key=file_key)
logger.exception(msg)
context = {
'submission': create_submission_dict(submission, self.prompts) if submission else None,
'rubric_criteria': copy.deepcopy(self.rubric_criteria_with_labels),
'student_username': student_username,
}
return context
def get_student_info_path_and_context(self, student_username, expanded_view=None):
"""
......@@ -238,12 +313,10 @@ class StaffAreaMixin(object):
expanded_view (str): An optional view to be shown initially expanded.
The default is None meaning that all views are shown collapsed.
"""
submission_uuid = None
submission = None
assessment_steps = self.assessment_steps
anonymous_user_id = None
submissions = None
student_item = None
submission_uuid = None
submission = None
if student_username:
anonymous_user_id = self.get_anonymous_user_id(student_username, self.course_id)
......@@ -253,24 +326,12 @@ class StaffAreaMixin(object):
# If there is a submission available for the requested student, present
# it. If not, there will be no other information to collect.
submissions = submission_api.get_submissions(student_item, 1)
if submissions:
submission_uuid = submissions[0]['uuid']
submission = submissions[0]
submission_uuid = submission['uuid']
if 'file_key' in submission.get('answer', {}):
file_key = submission['answer']['file_key']
context = self.get_student_submission_context(student_username, submission)
try:
submission['file_url'] = file_api.get_download_url(file_key)
except file_exceptions.FileUploadError:
# Log the error, but do not prevent the rest of the student info
# from being displayed.
msg = (
u"Could not retrieve image URL for staff debug page. "
u"The learner username is '{student_username}', and the file key is {file_key}"
).format(student_username=student_username, file_key=file_key)
logger.exception(msg)
assessment_steps = self.assessment_steps
example_based_assessment = None
self_assessment = None
......@@ -291,8 +352,7 @@ class StaffAreaMixin(object):
workflow_cancellation = self.get_workflow_cancellation_info(submission_uuid)
context = {
'submission': create_submission_dict(submission, self.prompts) if submission else None,
context.update({
'score': workflow.get('score'),
'workflow_status': workflow.get('status'),
'workflow_cancellation': workflow_cancellation,
......@@ -300,10 +360,8 @@ class StaffAreaMixin(object):
'submitted_assessments': submitted_assessments,
'self_assessment': self_assessment,
'example_based_assessment': example_based_assessment,
'rubric_criteria': copy.deepcopy(self.rubric_criteria_with_labels),
'student_username': student_username,
'expanded_view': expanded_view,
}
})
if peer_assessments or self_assessment or example_based_assessment:
max_scores = peer_api.get_rubric_max_scores(submission_uuid)
......
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.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -23,7 +23,7 @@
/**
* Load the staff area.
*/
load: function() {
load: function(onSuccessCallback) {
var view = this;
// If we're course staff, the base template should contain a section
......@@ -35,6 +35,9 @@
$('.openassessment__staff-area', view.element).replaceWith(html);
view.server.renderLatex($('.openassessment__staff-area', view.element));
view.installHandlers();
if (onSuccessCallback) {
onSuccessCallback();
}
}).fail(function() {
view.baseView.showLoadError('staff_area');
});
......@@ -115,6 +118,54 @@
return deferred.promise();
},
loadStaffGradeForm: function(eventObject) {
var view = this;
var staff_form_element = $(eventObject.currentTarget);
var isCollapsed = staff_form_element.hasClass("is--collapsed");
var deferred = $.Deferred();
if (isCollapsed && !this.staffGradeFormLoaded) {
eventObject.preventDefault();
this.staffGradeFormLoaded = true;
this.server.staffGradeForm().done(function(html) {
// TODO: need to create a div to show an error message if this server call fails.
//showFormError('');
// Load the HTML and install event handlers
$('.staff__grade__form', view.element).replaceWith(html);
// Initialize the rubric : TODO SHARE CODE!
var $rubric = $('.staff-assessment__assessment', view.element);
if ($rubric.size() > 0) {
var rubricElement = $rubric.get(0);
var rubric = new OpenAssessment.Rubric(rubricElement);
// Install a change handler for rubric options to enable/disable the submit button
rubric.canSubmitCallback($.proxy(view.staffSubmitEnabled, view));
// Install a click handler for the submit buttons
$('.wrapper--staff-assessment .action--submit', view.element).click(
function(eventObject) {
var submissionID = staff_form_element.find('.staff__grade__form').data('submission-uuid'); // This was a change
eventObject.preventDefault();
view.submitStaffGradeForm(submissionID, rubric,
$(eventObject.currentTarget).hasClass('continue_grading--action')
); // This was a change
}
);
}
deferred.resolve();
}).fail(function() {
// showFormError(gettext('Unexpected server error.'));
this.staffGradeFormLoaded = false;
deferred.reject();
});
}
},
/**
* Install event handlers for the view.
*/
......@@ -123,6 +174,7 @@
var $staffArea = $('.openassessment__staff-area', this.element);
var $staffTools = $('.openassessment__staff-tools', $staffArea);
var $staffInfo = $('.openassessment__student-info', $staffArea);
var $staffGradeTool = $('.openassessment__staff-grading', $staffArea);
if ($staffArea.length <= 0) {
return;
......@@ -130,6 +182,7 @@
this.baseView.setUpCollapseExpand($staffTools, function() {});
this.baseView.setUpCollapseExpand($staffInfo, function() {});
this.baseView.setUpCollapseExpand($staffGradeTool, function() {});
// Install a click handler for the staff button panel
$staffArea.find('.ui-staff__button').click(
......@@ -190,6 +243,16 @@
view.rescheduleUnfinishedTasks();
}
);
// Install a click handler for showing the staff grading form.
$staffGradeTool.find('.staff__grade__control').click(
function(eventObject) {
// Don't call preventDefault because this click handler will be triggerred by
// things like the radio buttons within the form-- we want them to be able to
// handle the click events (and loadStaffGradeForm will be a no-op).
view.loadStaffGradeForm(eventObject);
}
);
},
/**
......@@ -318,8 +381,6 @@
submitStaffAssessment: function(submissionID, rubric) {
// Send the assessment to the server
var view = this;
var baseView = this.baseView;
baseView.toggleActionError('staff', null);
view.staffSubmitEnabled(false);
this.server.staffAssess(
......@@ -332,7 +393,32 @@
// the staff override itself.
view.loadStudentInfo({expanded_view: 'final-grade'});
}).fail(function(errorMessage) {
$('.staff-override-error').html(_.escape(errorMessage));
// TODO: check how this renders Andy!
$('.staff-override-error', this.element).html(_.escape(errorMessage));
view.staffSubmitEnabled(true);
});
},
// Can some version of this method be shared with submitStaffAssessment?
submitStaffGradeForm: function(submissionID, rubric, continueGrading) {
// Send the assessment to the server
var view = this;
view.staffSubmitEnabled(false);
this.server.staffAssess(
rubric.optionsSelected(), rubric.criterionFeedback(), rubric.overallFeedback(), submissionID
).done(function() {
view.staffGradeFormLoaded = false;
var onSuccessCallback = function () {
$('.button-staff-grading').click();
if (continueGrading) {
$('.staff__grade__title').click();
}
};
view.load(onSuccessCallback);
}).fail(function(errorMessage) {
// TODO: this needs styling help Andy!
$('.staff-grade-error', this.element).html(_.escape(errorMessage));
view.staffSubmitEnabled(true);
});
}
......
......@@ -116,6 +116,29 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
},
/**
* Load the student information section inside the Staff Info section. TODO update
*
* @param {string} studentUsername - The username for the student.
* @param {object} options - An optional set of configuration options.
* @returns {promise} A JQuery promise, which resolves with the HTML of the rendered section
* fails with an error message.
*/
staffGradeForm: function() {
var url = this.url('render_staff_grade_form');
return $.Deferred(function(defer) {
$.ajax({
url: url,
type: "POST",
dataType: "html"
}).done(function(data) {
defer.resolveWith(this, [data]);
}).fail(function() {
defer.rejectWith(this, [gettext('The staff grade form could not be loaded.')]);
});
}).promise();
},
/**
* Send a submission to the XBlock.
*
* @param {string} submission The text of the student's submission.
......
......@@ -157,6 +157,99 @@
}
// staff grade header
.ui-staff {
.staff__grade__control {
padding: 0 (3*$baseline-h/4);
border-top: ($baseline-v/4) solid $color-decorative-tertiary;
background: $bg-content;
// step title
h3.staff__grade__title {
@include text-align(left);
@include float(none);
margin-top: 0 !important;
display: block;
width: 100%;
.staff__grade__label {
@extend %t-superheading;
text-transform: none;
letter-spacing: normal;
}
}
// staff grade status
.staff__grade__status {
display: inline-block;
margin-top: ($baseline-v/4);
@include media($bp-dm) {
margin-top: 0;
@include float(right);
position: relative;
top: -($baseline-v*2);
}
@include media($bp-dl) {
margin-top: 0;
@include float(right);
position: relative;
top: -($baseline-v*2);
}
@include media($bp-dx) {
margin-top: 0;
@include float(right);
position: relative;
top: -($baseline-v*2);
}
.staff__grade__value {
border-radius: ($baseline-v/10);
padding: ($baseline-v/4) ($baseline-h/4);
background: $color-decorative-tertiary;
position: relative;
@include media($bp-ds) {
display: block;
}
@include media($bp-dm) {
display: block;
}
@include media($bp-dl) {
display: block;
}
@include media($bp-dx) {
display: block;
}
}
.copy {
@extend %t-score;
color: $heading-color;
}
}
.submit_assessment--action {
display: inline;
}
}
// Override the default color for h3 (for elements that can be toggled).
.ui-toggle-visibility .ui-toggle-visibility__control .staff__grade__title{
color: $action-primary-color;
}
}
// UI - cancel submission (action)
.staff-info__workflow-cancellation {
......
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