Commit 1aee957a by Christina Roberts

Merge pull request #818 from edx/christina/full-grade

Full staff grading UI
parents 29cd3969 d8d093af
......@@ -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': { ... }
}
"""
......
......@@ -19,7 +19,7 @@
<div class="staff-info__content ui-staff__content">
<div class="staff-info__student ui-staff__content__section">
<div class="staff-info__student">
<div class="wrapper--input" class="staff-info__student__form">
<form class="openassessment_student_info_form">
<div class="form--error"></div>
......@@ -167,6 +167,7 @@
</div>
</div>
{% 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 +176,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 %}
<div class="staff__grade__control ui-toggle-visibility is--collapsed">
<header class="staff__grade__header ui-toggle-visibility__control">
<h3 class="staff__grade__title">
<span class="wrapper--copy">
<button class="staff__grade__show-form">{% trans "Staff Assessment" %}</button>
</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="ui-staff__content__section staff__grade__content ui-toggle-visibility__content">
<div class="wrapper--input">
<div class="staff__grade__form--error"></div>
<div class="staff__grade__form"></div>
</div>
</div>
</div>
{% 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>
<p>{% trans "Give this learner a grade using the problem's rubric." %}</p>
</div>
<div>
<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" %}
</form>
</article>
</div>
<div>
<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 continue grading" %}</span>
</button>
</li>
</ul>
<div class="staff-grade-error"></div>
</div>
</div>
</div>
{% endblock %}
{% endspaceless %}
......@@ -2,52 +2,49 @@
{% spaceless %}
{% block body %}
<div class="ui-toggle-visibility__content">
<div class="wrapper--staff-assessment">
<div class="step__instruction">
<p>{% trans "Override this learner's current grade using the problem's rubric." %}</p>
</div>
<div class="wrapper--staff-assessment">
<div class="step__instruction">
<p>{% trans "Override this learner's current 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">
<h3 class="staff-assessment__display__title">
{% blocktrans %}
Response for: {{ student_username }}
{% endblocktrans %}
</h3>
</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__content">
<article class="staff-assessment">
<div class="staff-assessment__display">
<header class="staff-assessment__display__header">
<h3 class="staff-assessment__display__title">
{% blocktrans %}
Response for: {{ student_username }}
{% endblocktrans %}
</h3>
</header>
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label="The learner's response to the question above:" %}
<div class="step__actions">
<div class="message message--inline message--error message--error-server">
<h3 class="message__title">{% trans "We could not submit your assessment" %}</h3>
<div class="message__content"></div>
{% 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>
<ul class="list list--actions">
<li class="list--actions__item">
<button type="submit" class="action action--submit is--disabled">
<span class="copy">{% trans "Submit your assessment" %}</span>
</button>
<form class="staff-assessment__assessment" method="post">
{% include "openassessmentblock/oa_rubric.html" with rubric_type="staff" %}
</form>
</article>
</div>
<div class="staff-override-error"></div>
</li>
</ul>
<div class="step__actions">
<div class="message message--inline message--error message--error-server">
<h3 class="message__title">{% trans "We could not submit your assessment" %}</h3>
<div class="message__content"></div>
</div>
<ul class="list list--actions">
<li class="list--actions__item">
<button type="submit" class="action action--submit is--disabled">
<span class="copy">{% trans "Submit assessment" %}</span>
</button>
<div class="staff-override-error"></div>
</li>
</ul>
</div>
</div>
{% endblock %}
{% endspaceless %}
......@@ -32,14 +32,16 @@
{% endblocktrans %}
</p>
{% else %}
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label="The learner's response to the question above:" %}
<div class="wrapper--content">
{% include "openassessmentblock/oa_submission_answer.html" with answer=submission.answer answer_text_label="The learner's response to the question above:" %}
{% if submission.file_url %}
<a href="{{ submission.file_url }}" class="submission--file">
{% trans "The file associated with this response." %}
</a>
<span>{% trans "Caution: This file was uploaded by another course learner and has not been verified, screened, approved, reviewed, or endorsed by edX. If you decide to access it, you do so at your own risk." %}</span>
{% endif %}
{% if submission.file_url %}
<a href="{{ submission.file_url }}" class="submission--file">
{% trans "The file associated with this response." %}
</a>
<span>{% trans "Caution: This file was uploaded by another course learner and has not been verified, screened, approved, reviewed, or endorsed by edX. If you decide to access it, you do so at your own risk." %}</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
......@@ -265,7 +267,7 @@
<div class="staff-info__staff-override__content ui-toggle-visibility__content">
<div class="wrapper--input">
{% include "openassessmentblock/staff_area/oa_staff_assessment.html" %}
{% include "openassessmentblock/staff_area/oa_staff_override_assessment.html" %}
</div>
</div>
</div>
......
......@@ -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
......@@ -77,6 +78,7 @@ def require_course_staff(error_key, with_json_handler=False):
permission_errors = {
"STAFF_AREA": xblock._(u"You do not have permission to access the ORA staff area"),
"STUDENT_INFO": xblock._(u"You do not have permission to access ORA learner information."),
"STUDENT_GRADE": xblock._(u"You do not have permission to access ORA staff grading."),
}
if not xblock.is_course_staff and with_json_handler:
......@@ -164,7 +166,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 +235,83 @@ 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 getting learner information."))
@XBlock.handler
@require_course_staff("STUDENT_GRADE")
def render_staff_grade_form(self, data, suffix=''): # pylint: disable=W0613
"""
Renders a form to staff-grade the next available learner submission.
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']
# Note that this will check out a submission for grading by the specified staff member.
# If no submissions are available for grading, will return None.
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"Error loading the checked out learner response."))
else:
return self.render_error(self._(u"No other learner responses are available for grading at this time."))
except PeerAssessmentInternalError:
return self.render_error(self._(u"Error getting staff grade information."))
def get_student_submission_context(self, student_username, submission):
"""
Get a context dict for rendering a student submission and associated rubric (for staff grading).
Includes submission (populating submitted file information if relevant), rubric_criteria,
and student_username.
Args:
student_username (unicode): The username of the student to report.
submission (object): A submission, as returned by the submission_api.
Returns:
A context dict for rendering a student submission and associated rubric (for staff grading).
"""
if submission and '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,
}
if self.rubric_feedback_prompt is not None:
context["rubric_feedback_prompt"] = self.rubric_feedback_prompt
if self.rubric_feedback_default_text is not None:
context['rubric_feedback_default_text'] = self.rubric_feedback_default_text
return context
def get_student_info_path_and_context(self, student_username, expanded_view=None):
"""
......@@ -238,12 +323,11 @@ 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
submissions = None
submission = None
submission_uuid = None
if student_username:
anonymous_user_id = self.get_anonymous_user_id(student_username, self.course_id)
......@@ -255,22 +339,12 @@ class StaffAreaMixin(object):
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 +365,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 +373,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.
......@@ -689,6 +689,8 @@
"template": "openassessmentblock/staff_area/oa_staff_area.html",
"context": {
"staff_assessment_required": true,
"staff_assessment_ungraded": 10,
"staff_assessment_in_progress": 2,
"status_counts": {
"self": 1,
"peer": 2,
......@@ -718,6 +720,40 @@
"output": "oa_staff_area_full_grading.html"
},
{
"template": "openassessmentblock/staff_area/oa_staff_area.html",
"context": {
"staff_assessment_required": true,
"staff_assessment_ungraded": 9,
"staff_assessment_in_progress": 0,
"status_counts": {
"self": 1,
"peer": 2,
"waiting": 3,
"done": 4
},
"num_submissions": 10,
"item_id": "test_item",
"step_dates": [
{
"step": "submission",
"start": "2014-01-01",
"due": "N/A"
},
{
"step": "peer",
"start": "2014-02-02",
"due": "N/A"
},
{
"step": "self",
"start": "2014-03-03",
"due": "2015-04-05"
}
]
},
"output": "oa_staff_area_full_grading_2.html"
},
{
"template": "openassessmentblock/staff_area/oa_student_info.html",
"context": {
"rubric_criteria": [
......@@ -1031,5 +1067,117 @@
]
},
"output": "oa_turbo_mode.html"
},
{
"template": "openassessmentblock/staff_area/oa_staff_grade_learners_assessment.html",
"context": {
"rubric_criteria": [
{
"name": "vocabulary",
"prompt": "vocabulary",
"order_num": 0,
"feedback": "optional",
"options": [
{
"order_num": 0,
"points": 0,
"name": "Bad"
},
{
"order_num": 1,
"points": 1,
"name": "Good"
}
]
},
{
"name": "grammar",
"prompt": "grammar",
"order_num": 1,
"options": [
{
"order_num": 0,
"points": 0,
"name": "Bad"
},
{
"order_num": 1,
"points": 1,
"name": "Good"
}
]
},
{
"name": "feedback_only",
"prompt": "Feedback only, no options!",
"order_num": 2,
"feedback": "required",
"options": []
}
],
"submission": {
"answer": {
"text": "testing response text"
}
},
"student_username": "mock_user"
},
"output": "oa_staff_grade_learners_assessment.html"
},
{
"template": "openassessmentblock/staff_area/oa_staff_grade_learners_assessment.html",
"context": {
"rubric_criteria": [
{
"name": "vocabulary",
"prompt": "vocabulary",
"order_num": 0,
"feedback": "optional",
"options": [
{
"order_num": 0,
"points": 0,
"name": "Bad"
},
{
"order_num": 1,
"points": 1,
"name": "Good"
}
]
},
{
"name": "grammar",
"prompt": "grammar",
"order_num": 1,
"options": [
{
"order_num": 0,
"points": 0,
"name": "Bad"
},
{
"order_num": 1,
"points": 1,
"name": "Good"
}
]
},
{
"name": "feedback_only",
"prompt": "Feedback only, no options!",
"order_num": 2,
"feedback": "required",
"options": []
}
],
"submission": {
"answer": {
"text": "testing response text"
}
},
"student_username": "mock_user_2"
},
"output": "oa_staff_grade_learners_assessment_2.html"
}
]
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -116,6 +116,27 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
},
/**
* Renders the next submission for staff grading.
*
* @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 assessment form could not be loaded.')]);
});
}).promise();
},
/**
* Send a submission to the XBlock.
*
* @param {string} submission The text of the student's submission.
......
......@@ -4,8 +4,7 @@
// NOTES:
// * staff-centric UI used for reporting/debugging
.wrapper--xblock {
.openassessment {
// --------------------
// general: staff UI
......@@ -81,6 +80,12 @@
.ui-staff__content__section {
padding-bottom: ($baseline-v/2);
@extend %wipe-last-child;
.wrapper--input,
.wrapper--content {
padding: $baseline-v ($baseline-h/2);
background-color: $bg-content;
}
}
// --------------------
......@@ -105,9 +110,18 @@
}
}
// UI - status (table)
.staff-info__status, .staff-info__classifierset {
.openassessment_student_info_form {
margin-bottom: ($baseline-v/2);
.list--actions__item {
@include text-align(left);
.action--submit-username {
@extend %btn--secondary;
margin-left: 0;
}
}
}
.staff-info__status__table, .staff-info__classifierset__table {
......@@ -155,29 +169,186 @@
}
}
}
// staff assessments
.wrapper--staff-assessment {
margin-top: ($baseline-v/2);
padding-top: ($baseline-v/2);
border-top: 1px solid $color-decorative-tertiary;
}
.staff-assessment__display {
@extend %ui-subsection;
}
.staff-assessment__display__header {
@include clearfix();
span {
@extend %t-strong; // FIX: needed due to DOM structure
}
// UI - cancel submission (action)
.staff-info__workflow-cancellation {
.staff-assessment__display__title {
@extend %t-heading;
margin-bottom: ($baseline-v/2);
color: $heading-secondary-color;
}
}
.staff-info__cancel-submission__content {
.staff-assessment__display__response {
@extend %ui-subsection-content;
@extend %copy-3;
@extend %ui-content-longanswer;
@extend %ui-well;
color: $copy-color;
}
.comments__label {
color: $copy-secondary-color;
// assessment form
.staff-assessment__assessment {
// fields
.assessment__fields {
margin-bottom: $baseline-v;
}
// rubric question
.assessment__rubric__question {
@extend %ui-rubric-question;
}
.cancel_submission_comments {
width: 100%;
min-height: ($baseline-v*5);
text-align: left;
// rubric options
.question__answers {
@extend %ui-rubric-answers;
}
.list--actions {
.action--submit {
margin: ($baseline-v/2) 0;
// general feedback question
.assessment__rubric__question--feedback {
.wrapper--input {
margin-top: $baseline-v;
}
.question__title__copy {
@include margin-left(0);
white-space: pre-wrap;
}
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*5);
}
}
}
// Styling for staff grade tab ("Grade Available Responses").
.ui-staff {
.staff__grade__control {
border-top: ($baseline-v/4) solid $color-decorative-tertiary;
background: $bg-content;
background-color: $bg-content;
.staff__grade__title {
@include text-align(left);
@include float(none);
padding: 0 $baseline-v;
display: block;
width: 100%;
.staff__grade__show-form {
@extend %btn-reset;
@extend %t-superheading;
@include fontSize($f-size-medium);
background-color: $bg-content;
text-transform: none;
letter-spacing: normal;
padding: 0;
}
}
.staff__grade__status {
display: inline-block;
padding: 0 $baseline-v;
@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;
line-height: 0;
@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;
}
.wrapper--input {
padding-top: 0;
}
}
// 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 {
.staff-info__cancel-submission__content {
.comments__label {
color: $copy-secondary-color;
}
.cancel_submission_comments {
width: 100%;
min-height: ($baseline-v*5);
text-align: left;
}
}
}
}
......@@ -44,23 +44,14 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
padding: 10px;
.ui-staff__button {
margin-left: ($baseline-v/2);
@extend %btn-reset;
padding: ($baseline-v/4) ($baseline-v/2);
border-radius: ($baseline-v/4);
text-transform: uppercase;
color: $lighter-base-font-color;
background-color: $shadow-l2;
// Remove button styling
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 12px;
-webkit-appearance: none;
background-image: none;
text-shadow: none;
box-shadow: none;
border: none;
border-image: none;
&.is--active {
color: white;
background-color: $edx-pink;
......@@ -103,14 +94,6 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
}
}
.staff-info__status {
.action--submit {
@extend %btn--secondary;
@extend %action-2;
@include margin(($baseline-v/2), ($baseline-v/2), ($baseline-v/2), ($baseline-v/2));
}
}
.staff-info__student {
.label {
color: $heading-staff-color;
......@@ -121,12 +104,6 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
}
}
.action--submit {
@extend %btn--secondary;
@extend %action-2;
@include margin(($baseline-v/2), ($baseline-v/2), ($baseline-v/2), ($baseline-v/2));
}
.title {
@extend %hd-2;
color: $heading-staff-color;
......@@ -161,89 +138,9 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
}
}
.staff-info__cancel-submission__content,
.staff-info__staff-override__content {
padding: $baseline-v ($baseline-h/2);
background-color: white;
}
.value {
width: $max-width/2;
}
// staff assessments
.wrapper--staff-assessment {
margin-top: ($baseline-v/2);
padding-top: ($baseline-v/2);
border-top: 1px solid $color-decorative-tertiary;
.action--submit {
@extend .action--submit;
}
}
.staff-assessment__display {
@extend %ui-subsection;
}
.staff-assessment__display__header {
@include clearfix();
span {
@extend %t-strong; // FIX: needed due to DOM structure
}
.staff-assessment__display__title {
@extend %t-heading;
margin-bottom: ($baseline-v/2);
color: $heading-secondary-color;
}
}
.staff-assessment__display__response {
@extend %ui-subsection-content;
@extend %copy-3;
@extend %ui-content-longanswer;
@extend %ui-well;
color: $copy-color;
}
// assessment form
.staff-assessment__assessment {
// fields
.assessment__fields {
margin-bottom: $baseline-v;
}
// rubric question
.assessment__rubric__question {
@extend %ui-rubric-question;
}
// rubric options
.question__answers {
@extend %ui-rubric-answers;
}
// general feedback question
.assessment__rubric__question--feedback {
.wrapper--input {
margin-top: $baseline-v;
}
.question__title__copy {
@include margin-left(0);
white-space: pre-wrap;
}
textarea {
@extend %ui-content-longanswer;
min-height: ($baseline-v*5);
}
}
}
}
// --------------------
......@@ -1211,14 +1108,7 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
max-width: 100%;
}
// Developer SASS for Continued Grading.
.openassessment__steps__step {
.action--continue--grading {
@extend .action--submit;
}
}
#openassessment__leaderboard{
#openassessment__leaderboard {
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
.step__counter, .step__counter:before {
......
......@@ -196,11 +196,11 @@
.staff-info__workflow-cancellation {
margin-bottom: ($baseline-v) !important;
}
}
.staff-info__student {
// 'p' elements in LMS have a color set on them.
.student__answer__display__content p {
color: inherit;
.wrapper--staff-assessment {
// 'p' elements in LMS have a color set on them.
.student__answer__display__content p {
color: inherit;
}
}
}
......@@ -29,6 +29,56 @@
padding: 0;
}
// --------------------
// overall actions
// --------------------
.list--actions {
margin-bottom: ($baseline-v/2);
@include text-align(center);
@include media($bp-ds) {
@include text-align(right);
}
@include media($bp-dm) {
@include text-align(right);
}
@include media($bp-dl) {
@include text-align(right);
}
@include media($bp-dx) {
@include text-align(right);
}
// STATE: actions has an error
&.has--error {
.message {
margin-bottom: $baseline-v;
@include text-align(left);
}
}
.action--submit {
@extend %btn--primary;
@extend %action-2;
margin-left: ($baseline-v/2);
.copy, .icon {
display: inline;
}
.icon {
@extend %icon-2;
}
.fa-caret-right:before {
@include transform(rotate(bidi-rotate-angle(0deg)));
}
}
}
// --------------------
// steps
......@@ -235,55 +285,6 @@
}
}
// step actions
.step__actions {
margin-bottom: ($baseline-v/2);
@include text-align(center);
@include media($bp-ds) {
@include text-align(right);
}
@include media($bp-dm) {
@include text-align(right);
}
@include media($bp-dl) {
@include text-align(right);
}
@include media($bp-dx) {
@include text-align(right);
}
.action--submit {
@extend %btn--primary;
@extend %action-2;
.copy, .icon {
display: inline;
}
.icon {
@extend %icon-2;
}
.fa-caret-right:before {
@include transform(rotate(bidi-rotate-angle(0deg)));
}
}
// STATE: actions has an error
&.has--error {
.message {
margin-bottom: $baseline-v;
@include text-align(left);
}
}
}
// STATE: step is loading
&.is--loading {
......@@ -540,6 +541,10 @@
display: inline-block;
vertical-align: middle;
@include margin-left(($baseline-h/4));
.list--actions {
@include text-align(left);
}
}
.response__submission__status__title {
......@@ -549,11 +554,14 @@
.response__submission__actions {
.list--actions__item {
@include text-align(left);
}
.action--save {
@extend %btn--secondary;
@extend %action-2;
display: block;
@include text-align(center);
margin-bottom: ($baseline-v/2);
min-width: 215px;
......@@ -1125,7 +1133,7 @@
@extend %ui-subsection-content;
padding-top: 0;
.list--actions {
.list--actions {
padding: 0;
}
......
......@@ -106,6 +106,16 @@
border-radius: ($baseline-v/10);
}
%btn-reset {
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 12px;
-webkit-appearance: none;
background-image: none;
text-shadow: none;
box-shadow: none;
border: none;
border-image: none;
}
// buttons - primary (assuming dark bg/light copy)
%btn--primary {
......
<openassessment>
<title>Open Assessment Test</title>
<prompts>
<prompt>
<description>Given the state of the world today, what do you think should be done to combat poverty? Please answer in a short essay of 200-300 words.</description>
</prompt>
<prompt>
<description>Given the state of the world today, what do you think should be done to combat pollution?</description>
</prompt>
</prompts>
<rubric>
<criterion>
<name>Concise</name>
<prompt>How concise is it?</prompt>
<option points="0">
<name>Neal Stephenson (late)</name>
<explanation>Neal Stephenson explanation</explanation>
</option>
<option points="1">
<name>HP Lovecraft</name>
<explanation>HP Lovecraft explanation</explanation>
</option>
<option points="3">
<name>Robert Heinlein</name>
<explanation>Robert Heinlein explanation</explanation>
</option>
<option points="4">
<name>Neal Stephenson (early)</name>
<explanation>Neal Stephenson (early) explanation</explanation>
</option>
<option points="5">
<name>Earnest Hemingway</name>
<explanation>Earnest Hemingway</explanation>
</option>
</criterion>
<criterion>
<name>Clear-headed</name>
<prompt>How clear is the thinking?</prompt>
<option points="0">
<name>Yogi Berra</name>
<explanation>Yogi Berra explanation</explanation>
</option>
<option points="1">
<name>Hunter S. Thompson</name>
<explanation>Hunter S. Thompson explanation</explanation>
</option>
<option points="2">
<name>Robert Heinlein</name>
<explanation>Robert Heinlein explanation</explanation>
</option>
<option points="3">
<name>Isaac Asimov</name>
<explanation>Isaac Asimov explanation</explanation>
</option>
<option points="10">
<name>Spock</name>
<explanation>Spock explanation</explanation>
</option>
</criterion>
<criterion>
<name>Form</name>
<prompt>Lastly, how is its form? Punctuation, grammar, and spelling all count.</prompt>
<option points="0">
<name>lolcats</name>
<explanation>lolcats explanation</explanation>
</option>
<option points="1">
<name>Facebook</name>
<explanation>Facebook explanation</explanation>
</option>
<option points="2">
<name>Reddit</name>
<explanation>Reddit explanation</explanation>
</option>
<option points="3">
<name>metafilter</name>
<explanation>metafilter explanation</explanation>
</option>
<option points="4">
<name>Usenet, 1996</name>
<explanation>Usenet, 1996 explanation</explanation>
</option>
<option points="5">
<name>The Elements of Style</name>
<explanation>The Elements of Style explanation</explanation>
</option>
</criterion>
</rubric>
<assessments>
<assessment name="staff-assessment" />
</assessments>
</openassessment>
......@@ -608,6 +608,88 @@ class TestCourseStaff(XBlockHandlerTestCase):
self.assertIn("The learner submission has been removed from peer", resp['msg'])
self.assertEqual(True, resp['success'])
@scenario('data/staff_grade_scenario.xml', user_id='Bob')
def test_staff_assessment_counts(self, xblock):
"""
Verify the staff assessment counts (ungraded and checked out)
as shown in the staff grading tool when staff assessment is required.
"""
_, context = xblock.get_staff_path_and_context()
self._verify_staff_assessment_context(context, True, 0, 0)
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, False, "Bob"
)
bob_item = STUDENT_ITEM.copy()
bob_item["item_id"] = xblock.scope_ids.usage_id
# Create a submission for Bob, and corresponding workflow.
self._create_submission(bob_item, {'text': "Bob Answer"}, [])
# Verify the count as shown in the staff grading tool.
_, context = xblock.get_staff_path_and_context()
self._verify_staff_assessment_context(context, True, 1, 0)
# Check out the assessment for grading and ensure that the count changes.
self.request(xblock, 'render_staff_grade_form', json.dumps({}))
_, context = xblock.get_staff_path_and_context()
self._verify_staff_assessment_context(context, True, 0, 1)
@scenario('data/example_based_assessment.xml', user_id='Bob')
def test_staff_assessment_counts_not_required(self, xblock):
"""
Verify the staff assessment counts (ungraded and checked out) are
not present in the context when staff assessment is not required.
"""
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, True, "Bob"
)
_, context = xblock.get_staff_path_and_context()
self._verify_staff_assessment_context(context, False)
@scenario('data/staff_grade_scenario.xml', user_id='Bob')
def test_staff_assessment_form(self, xblock):
"""
Smoke test that the staff assessment form renders when staff assessment
is required.
"""
permission_denied = "You do not have permission to access ORA staff grading."
no_submissions_available = "No other learner responses are available for grading at this time."
submission_text = "Grade me, please!"
resp = self.request(xblock, 'render_staff_grade_form', json.dumps({})).decode('utf-8')
self.assertIn(permission_denied, resp)
self.assertNotIn(no_submissions_available, resp)
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, False, "Bob"
)
resp = self.request(xblock, 'render_staff_grade_form', json.dumps({})).decode('utf-8')
self.assertNotIn(permission_denied, resp)
self.assertIn(no_submissions_available, resp)
self.assertNotIn(submission_text, resp)
bob_item = STUDENT_ITEM.copy()
bob_item["item_id"] = xblock.scope_ids.usage_id
# Create a submission for Bob, and corresponding workflow.
self._create_submission(bob_item, {'text': submission_text}, [])
resp = self.request(xblock, 'render_staff_grade_form', json.dumps({})).decode('utf-8')
self.assertNotIn(no_submissions_available, resp)
self.assertIn(submission_text, resp)
def _verify_staff_assessment_context(self, context, required, ungraded=None, in_progress=None):
self.assertEquals(required, context['staff_assessment_required'])
if not required:
self.assertNotIn('staff_assessment_ungraded', context)
self.assertNotIn('staff_assessment_in_progress', context)
else:
self.assertEqual(ungraded, context['staff_assessment_ungraded'])
self.assertEqual(in_progress, context['staff_assessment_in_progress'])
def _create_mock_runtime(
self,
item_id,
......
......@@ -98,10 +98,10 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest):
"""
Test the accessibility of the staff area.
This is testing a problem with "self assessment only".
This is testing a problem with "staff assessment only".
"""
def setUp(self):
super(StaffAreaA11yTest, self).setUp('self_only', staff=True)
super(StaffAreaA11yTest, self).setUp('staff_only', staff=True)
self.staff_area_page = StaffAreaPage(self.browser, self.problem_loc)
def test_staff_tools_panel(self):
......@@ -120,12 +120,26 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest):
self.staff_area_page.click_staff_toolbar_button("staff-info")
self._check_a11y(self.staff_area_page)
def test_staff_grading_panel(self):
"""
Check the accessibility of the "Staff Grading" panel
"""
self.submission_page.visit().submit_response(self.SUBMISSION)
self.assertTrue(self.submission_page.has_submitted)
self.staff_area_page.visit()
self.staff_area_page.expand_staff_grading_section()
self._check_a11y(self.staff_area_page)
def test_learner_info(self):
"""
Check the accessibility of the learner information sections of the "Staff Tools" panel.
"""
# Create an assessment for a user.
username = self.do_self_assessment()
self.auto_auth_page.visit()
username = self.auto_auth_page.get_username()
self.submission_page.visit().submit_response(self.SUBMISSION)
self.assertTrue(self.submission_page.has_submitted)
self.staff_area_page.visit()
......@@ -139,24 +153,14 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest):
"""
Check the accessibility of the Staff Grade section, as shown to the learner.
"""
self.auto_auth_page.visit()
username = self.auto_auth_page.get_username()
self.submission_page.visit().submit_response(self.SUBMISSION)
self.assertTrue(self.submission_page.has_submitted)
# Submit a staff override
self.staff_area_page.visit()
self.staff_area_page.show_learner(username)
self.staff_area_page.expand_learner_report_sections()
self.staff_area_page.assess("staff", self.STAFF_OVERRIDE_OPTIONS_SELECTED)
self.do_staff_assessment(options_selected=self.STAFF_OVERRIDE_OPTIONS_SELECTED)
# Refresh the page, and learner completes a self-assessment.
# Then verify accessibility of the Staff Grade section (marked Complete).
# Refresh the page, then verify accessibility of the Staff Grade section (marked Complete).
self.browser.refresh()
self.self_asmnt_page.wait_for_page().wait_for_response()
self.self_asmnt_page.assess("self", self.OPTIONS_SELECTED).wait_for_complete()
self.assertTrue(self.self_asmnt_page.is_complete)
self._verify_staff_grade_section(self.STAFF_OVERRIDE_EXISTS, None)
self._verify_staff_grade_section(self.STAFF_GRADE_EXISTS, None)
self._check_a11y(self.staff_asmnt_page)
......@@ -169,7 +173,7 @@ class FullWorkflowA11yTest(OpenAssessmentA11yTest, FullWorkflowMixin):
"""
def setUp(self):
super(FullWorkflowA11yTest, self).setUp('full_workflow', staff=True)
super(FullWorkflowA11yTest, self).setUp('full_workflow_staff_override', staff=True)
self.staff_area_page = StaffAreaPage(self.browser, self.problem_loc)
def test_training_peer_self_staff_override(self):
......
......@@ -462,6 +462,13 @@ class StaffAreaPage(OpenAssessmentPage, AssessmentMixin):
panels = self.q(css=self._bounded_selector(".wrapper--ui-staff"))
return [panel.get_attribute('class') for panel in panels if u'is--hidden' not in panel.get_attribute('class')]
def is_button_visible(self, button_name):
"""
Returns True if button_name is visible, else False
"""
button = self.q(css=self._bounded_selector(".button-{button_name}".format(button_name=button_name)))
return button.is_present()
def click_staff_toolbar_button(self, button_name):
"""
Presses the button to show the panel with the specified name.
......@@ -491,6 +498,50 @@ class StaffAreaPage(OpenAssessmentPage, AssessmentMixin):
submit_button.first.click()
self.wait_for_element_visibility(".staff-info__student__report", "Student report is present")
def expand_staff_grading_section(self):
"""
Clicks the staff grade control to expand staff grading section for use in staff required workflows.
"""
self.click_staff_toolbar_button("staff-grading")
self.q(css=self._bounded_selector(".staff__grade__show-form")).first.click()
self.wait_for_element_visibility("#staff__assessment__rubric__question--0__0", "staff grading is present")
@property
def available_checked_out_numbers(self):
"""
Gets "N available and M checked out" information from staff grading sections.
Returns tuple of (N, M)
"""
if not 'GRADE AVAILABLE RESPONSES' in self.selected_button_names:
self.expand_staff_grading_section()
raw_string = self.q(css=self._bounded_selector(".staff__grade__value")).text[0]
ret = tuple(int(s) for s in raw_string.split() if s.isdigit())
if len(ret) != 2:
raise PageConfigurationError("Unable to parse available and checked out numbers")
return ret
def verify_available_checked_out_numbers(self, expected_value):
"""
Waits until the expected value for available and checked out numbers appears. If it does not appear, fails the test.
expected_value should be a tuple as described in the available_checked_out_numbers property above.
"""
EmptyPromise(
lambda: self.available_checked_out_numbers == expected_value,
"Expected avaiable and checked out values present"
).fulfill()
def submissions_available(self):
"""
Utility method to check if there are any more learner responses to grade in the staff grading section.
"""
found = self.q(
css=self._bounded_selector(".staff__grade__content")
)
if found.text[0] == "No other learner responses are available for grading at this time.":
return False
return True
@property
def learner_report_text(self):
"""
......@@ -557,11 +608,24 @@ class StaffAreaPage(OpenAssessmentPage, AssessmentMixin):
css=self._bounded_selector(".staff-info__student__response .ui-toggle-visibility__content")
).text[0]
def submit_assessment(self):
def staff_assess(self, options_selected, continue_after=False):
for criterion_num, option_num in enumerate(options_selected):
sel = "#staff__assessment__rubric__question--{criterion_num}__{option_num}".format(
assessment_type="staff",
criterion_num=criterion_num,
option_num=option_num
)
self.q(css=self._bounded_selector(sel)).first.click()
self.submit_assessment(continue_after)
def submit_assessment(self, continue_after=False):
"""
Submit a staff assessment of the problem.
"""
self.submit(button_css=".wrapper--staff-assessment .action--submit")
filter_text = "Submit assessment"
if continue_after:
filter_text += " and continue grading"
self.q(css=self._bounded_selector("button.action--submit")).filter(text=filter_text).first.click()
def cancel_submission(self):
"""
......
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