Commit 5a530dab by Andy Armstrong

First phase of staff re-grading UI

parent bf30363c
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
</ol> </ol>
{% if show_staff_area %} {% if show_staff_area %}
<div id="openassessment__staff-area"></div> <div class="openassessment__staff-area"></div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
> >
<h4 class="question__title ui-toggle-visibility__control"> <h4 class="question__title ui-toggle-visibility__control">
<i class="icon fa fa-caret-right"></i> <i class="icon fa fa-caret-right"></i>
<span class="ui-toggle-visibility__control__copy question__title__copy">{{ criterion.prompt }}</span> <span id="assessment__rubric__prompt--{{ criterion.order_num }}" class="ui-toggle-visibility__control__copy question__title__copy">{{ criterion.prompt }}</span>
<span class="label--required sr">* ({% trans "Required" %})</span> <span class="label--required sr">* ({% trans "Required" %})</span>
</h4> </h4>
...@@ -23,7 +23,8 @@ ...@@ -23,7 +23,8 @@
name="{{ criterion.name }}" name="{{ criterion.name }}"
id="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}" id="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
class="answer__value" class="answer__value"
value="{{ option.name }}" /> value="{{ option.name }}"
aria-labelledby="assessment__rubric__prompt--{{ criterion.order_num }}"/>
<label for="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}" <label for="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
class="answer__label" class="answer__label"
>{{ option.label }}</label> >{{ option.label }}</label>
......
...@@ -139,10 +139,10 @@ ...@@ -139,10 +139,10 @@
<ul class="list list--actions"> <ul class="list list--actions">
<li class="list--actions__item"> <li class="list--actions__item">
<a aria-role="button" href="#" id="step--response__submit" <button type="submit" id="step--response__submit"
class="action action--submit step--response__submit {{ submit_enabled|yesno:",is--disabled" }}"> class="action action--submit step--response__submit {{ submit_enabled|yesno:",is--disabled" }}">
<span class="copy">{% trans "Submit your response and move to the next step" %}</span> <span class="copy">{% trans "Submit your response and move to the next step" %}</span>
</a> </button>
</li> </li>
</ul> </ul>
</div> </div>
......
{% load i18n %} {% load i18n %}
{% load tz %} {% load tz %}
<div id="openassessment__staff-area" class="wrapper--staff-area"> <div class="openassessment__staff-area wrapper--staff-area">
<div id="openassessment__staff-toolbar" class="wrapper--staff-toolbar"> <div class="wrapper--staff-toolbar">
<button class="ui-staff__button button-staff-tools" data-panel="openassessment__staff-tools">{% trans "Staff Tools" %}</button> <button class="ui-staff__button button-staff-tools" data-panel="openassessment__staff-tools">{% trans "Staff Tools" %}</button>
<button class="ui-staff__button button-staff-info" data-panel="openassessment__staff-info">{% trans "Staff Info" %}</button> <button class="ui-staff__button button-staff-info" data-panel="openassessment__staff-info">{% trans "Staff Info" %}</button>
</div> </div>
<div id="openassessment__staff-tools" class="wrapper--staff-tools wrapper--ui-staff is--hidden"> <div class="openassessment__staff-tools wrapper--staff-tools wrapper--ui-staff is--hidden">
<div class="staff-info ui-staff"> <div class="staff-info ui-staff">
<h2 class="staff-info__title ui-staff__title"> <h2 class="staff-info__title ui-staff__title">
<span class="staff-info__title__copy">{% trans "Course Staff Tools" %}</span> <span class="staff-info__title__copy">{% trans "Course Staff Tools" %}</span>
...@@ -18,17 +18,20 @@ ...@@ -18,17 +18,20 @@
<div class="staff-info__student ui-staff__content__section"> <div class="staff-info__student ui-staff__content__section">
<div class="wrapper--input" class="staff-info__student__form"> <div class="wrapper--input" class="staff-info__student__form">
<form id="openassessment_student_info_form"> <form class="openassessment_student_info_form">
<label for="openassessment__student_username" class="label">{% trans "Enter an individual learner's username or email" %}</label> <div class="form--error"></div>
<input id="openassessment__student_username" type="text" class="value" maxlength="255"> <label class="label">{% trans "Enter an individual learner's username or email" %}
<input type="text" class="openassessment__student_username value" maxlength="255">
</label>
<ul class="list list--actions"> <ul class="list list--actions">
<li class="list--actions__item"> <li class="list--actions__item">
<a aria-role="button" href="" id="submit_student_username" class="action--submit"><span class="copy">{% trans "Submit" %}</span></a> <button class="action--submit action--submit-username"><span class="copy">{% trans "Submit" %}</span></button>
<div class="student-form-error"></div>
</li> </li>
</ul> </ul>
</form> </form>
</div> </div>
<div id="openassessment__student-info" class="staff-info__student__report"></div> <div class="openassessment__student-info staff-info__student__report"></div>
</div> </div>
{% if display_schedule_training %} {% if display_schedule_training %}
...@@ -65,22 +68,22 @@ ...@@ -65,22 +68,22 @@
</div> </div>
<div class="staff-info__status ui-staff__content__section"> <div class="staff-info__status ui-staff__content__section">
<a aria-role="button" href="" id="schedule_training" class="action--submit"><span class="copy">{% trans "Schedule Example-Based Assessment Training" %}</span></a> <button class="action--submit action--submit-training"><span class="copy">{% trans "Schedule Example-Based Assessment Training" %}</span></button>
<div id="schedule_training_message"></div> <div class="schedule_training_message"></div>
</div> </div>
{% endif %} {% endif %}
{% if display_reschedule_unfinished_tasks %} {% if display_reschedule_unfinished_tasks %}
<div class="staff-info__status ui-staff__content__section"> <div class="staff-info__status ui-staff__content__section">
<a aria-role="button" href="" id="reschedule_unfinished_tasks" class="action--submit"><span class="copy">{% trans "Reschedule All Unfinished Example-Based Assessment Grading Tasks" %}</span></a> <button class="action--submit action--submit-unfinished-tasks"><span class="copy">{% trans "Reschedule All Unfinished Example-Based Assessment Grading Tasks" %}</span></button>
<div id="reschedule_unfinished_tasks_message"></div> <div class="reschedule_unfinished_tasks_message"></div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<div id="openassessment__staff-info" class="wrapper--staff-info wrapper--ui-staff is--hidden"> <div class="openassessment__staff-info wrapper--staff-info wrapper--ui-staff is--hidden">
<div class="staff-info ui-staff"> <div class="staff-info ui-staff">
<h2 class="staff-info__title ui-staff__title"> <h2 class="staff-info__title ui-staff__title">
<span class="staff-info__title__copy">{% trans "Course Staff Information" %}</span> <span class="staff-info__title__copy">{% trans "Course Staff Information" %}</span>
......
{% load tz %}
{% load i18n %}
{% spaceless %}
{% block body %}
<div class="ui-toggle-visibility__content">
<div class="wrapper--staff-assessment">
<div class="step__instruction">
<p>{% trans "Allows you to override the current learner's 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_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">
<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 your assessment" %}</span>
</button>
</li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% endspaceless %}
...@@ -6,14 +6,9 @@ import logging ...@@ -6,14 +6,9 @@ import logging
from django.db import DatabaseError from django.db import DatabaseError
from openassessment.assessment.api import peer as peer_api from openassessment.assessment.errors import PeerAssessmentError, PeerAssessmentInternalError
from openassessment.assessment.api import ai as ai_api
from openassessment.assessment.api import student_training as training_api
from openassessment.assessment.errors import (
PeerAssessmentError, StudentTrainingInternalError, AIError,
PeerAssessmentInternalError)
from submissions import api as sub_api from submissions import api as sub_api
from .models import AssessmentWorkflow, AssessmentWorkflowCancellation, AssessmentWorkflowStep from .models import AssessmentWorkflow, AssessmentWorkflowCancellation
from .serializers import AssessmentWorkflowSerializer, AssessmentWorkflowCancellationSerializer from .serializers import AssessmentWorkflowSerializer, AssessmentWorkflowCancellationSerializer
from .errors import ( from .errors import (
AssessmentWorkflowError, AssessmentWorkflowInternalError, AssessmentWorkflowError, AssessmentWorkflowInternalError,
......
...@@ -24,6 +24,7 @@ from openassessment.assessment.api import self as self_api ...@@ -24,6 +24,7 @@ from openassessment.assessment.api import self as self_api
from openassessment.assessment.api import ai as ai_api from openassessment.assessment.api import ai as ai_api
from openassessment.fileupload import api as file_api from openassessment.fileupload import api as file_api
from openassessment.workflow import api as workflow_api from openassessment.workflow import api as workflow_api
from openassessment.workflow.models import AssessmentWorkflowCancellation
from openassessment.fileupload import exceptions as file_exceptions from openassessment.fileupload import exceptions as file_exceptions
...@@ -112,7 +113,7 @@ class StaffAreaMixin(object): ...@@ -112,7 +113,7 @@ class StaffAreaMixin(object):
Gets the path and context for the staff section of the ORA XBlock. Gets the path and context for the staff section of the ORA XBlock.
""" """
context = {} context = {}
path = 'openassessmentblock/staff_area/staff_area.html' path = 'openassessmentblock/staff_area/oa_staff_area.html'
student_item = self.get_student_item_dict() student_item = self.get_student_item_dict()
...@@ -214,16 +215,20 @@ class StaffAreaMixin(object): ...@@ -214,16 +215,20 @@ class StaffAreaMixin(object):
""" """
try: try:
student_username = data.params.get('student_username', '') student_username = data.params.get('student_username', '')
path, context = self.get_student_info_path_and_context(student_username) expanded_view = data.params.get('expanded_view', [])
path, context = self.get_student_info_path_and_context(
student_username,
expanded_view=expanded_view
)
return self.render_assessment(path, context) return self.render_assessment(path, context)
except PeerAssessmentInternalError: except PeerAssessmentInternalError:
return self.render_error(self._(u"Error finding assessment workflow cancellation.")) return self.render_error(self._(u"Error finding assessment workflow cancellation."))
def get_student_info_path_and_context(self, student_username): def get_student_info_path_and_context(self, student_username, expanded_view=None):
""" """
Get the proper path and context for rendering the the student info Get the proper path and context for rendering the student info
section of the staff debug panel. section of the staff area.
Args: Args:
student_username (unicode): The username of the student to report. student_username (unicode): The username of the student to report.
...@@ -278,19 +283,31 @@ class StaffAreaMixin(object): ...@@ -278,19 +283,31 @@ class StaffAreaMixin(object):
if "example-based-assessment" in assessment_steps: if "example-based-assessment" in assessment_steps:
example_based_assessment = ai_api.get_latest_assessment(submission_uuid) example_based_assessment = ai_api.get_latest_assessment(submission_uuid)
workflow = self.get_workflow_info(submission_uuid=submission_uuid)
workflow_cancellation = workflow_api.get_assessment_workflow_cancellation(submission_uuid) workflow_cancellation = workflow_api.get_assessment_workflow_cancellation(submission_uuid)
if workflow_cancellation: if workflow_cancellation:
workflow_cancellation['cancelled_by'] = self.get_username(workflow_cancellation['cancelled_by_id']) workflow_cancellation['cancelled_by'] = self.get_username(workflow_cancellation['cancelled_by_id'])
# Get the date that the workflow was cancelled to use in preference to the serialized date string
cancellation_model = AssessmentWorkflowCancellation.get_latest_workflow_cancellation(submission_uuid)
workflow_cancelled_at = cancellation_model.created_at
else:
workflow_cancelled_at = None
context = { context = {
'submission': create_submission_dict(submission, self.prompts) if submission else None, 'submission': create_submission_dict(submission, self.prompts) if submission else None,
'score': workflow.get('score'),
'workflow_status': workflow.get('status'),
'workflow_cancellation': workflow_cancellation, 'workflow_cancellation': workflow_cancellation,
'workflow_cancelled_at': workflow_cancelled_at,
'peer_assessments': peer_assessments, 'peer_assessments': peer_assessments,
'submitted_assessments': submitted_assessments, 'submitted_assessments': submitted_assessments,
'self_assessment': self_assessment, 'self_assessment': self_assessment,
'example_based_assessment': example_based_assessment, 'example_based_assessment': example_based_assessment,
'rubric_criteria': copy.deepcopy(self.rubric_criteria_with_labels), 'rubric_criteria': copy.deepcopy(self.rubric_criteria_with_labels),
'student_username': student_username 'student_username': student_username,
'expanded_view': expanded_view,
} }
if peer_assessments or self_assessment or example_based_assessment: if peer_assessments or self_assessment or example_based_assessment:
...@@ -298,7 +315,7 @@ class StaffAreaMixin(object): ...@@ -298,7 +315,7 @@ class StaffAreaMixin(object):
for criterion in context["rubric_criteria"]: for criterion in context["rubric_criteria"]:
criterion["total_value"] = max_scores[criterion["name"]] criterion["total_value"] = max_scores[criterion["name"]]
path = 'openassessmentblock/staff_area/student_info.html' path = 'openassessmentblock/staff_area/oa_student_info.html'
return path, context return path, context
@XBlock.json_handler @XBlock.json_handler
......
...@@ -651,7 +651,7 @@ ...@@ -651,7 +651,7 @@
"output": "oa_edit_student_training.html" "output": "oa_edit_student_training.html"
}, },
{ {
"template": "openassessmentblock/staff_area/staff_area.html", "template": "openassessmentblock/staff_area/oa_staff_area.html",
"context": { "context": {
"status_counts": { "status_counts": {
"self": 1, "self": 1,
...@@ -682,7 +682,7 @@ ...@@ -682,7 +682,7 @@
"output": "oa_staff_area.html" "output": "oa_staff_area.html"
}, },
{ {
"template": "openassessmentblock/staff_area/student_info.html", "template": "openassessmentblock/staff_area/oa_student_info.html",
"context": { "context": {
"submission": { "submission": {
"image_url": "/test-url", "image_url": "/test-url",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -50,7 +50,8 @@ describe("OpenAssessment.BaseView", function() { ...@@ -50,7 +50,8 @@ describe("OpenAssessment.BaseView", function() {
// Create a new stub server // Create a new stub server
server = new StubServer(); server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex') server.renderLatex = jasmine.createSpy('renderLatex');
// Create the object under test // Create the object under test
var el = $("#openassessment").get(0); var el = $("#openassessment").get(0);
view = new OpenAssessment.BaseView(runtime, el, server); view = new OpenAssessment.BaseView(runtime, el, server);
......
...@@ -26,16 +26,9 @@ describe("OpenAssessment.GradeView", function() { ...@@ -26,16 +26,9 @@ describe("OpenAssessment.GradeView", function() {
}; };
}; };
// Stub base view
var StubBaseView = function() {
this.showLoadError = function(msg) {};
this.toggleActionError = function(msg, step) {};
this.setUpCollapseExpand = function(sel) {};
};
// Stubs // Stubs
var baseView = null;
var server = null; var server = null;
var runtime = {};
// View under test // View under test
var view = null; var view = null;
...@@ -47,12 +40,10 @@ describe("OpenAssessment.GradeView", function() { ...@@ -47,12 +40,10 @@ describe("OpenAssessment.GradeView", function() {
// Create the stub server // Create the stub server
server = new StubServer(); server = new StubServer();
// Create the stub base view
baseView = new StubBaseView();
// Create and install the view // Create and install the view
var el = $('#openassessment-base').get(0); var gradeElement = $('#openassessment__grade').get(0);
view = new OpenAssessment.GradeView(el, server, baseView); var baseView = new OpenAssessment.BaseView(runtime, gradeElement, server, {});
view = new OpenAssessment.GradeView(gradeElement, server, baseView);
view.installHandlers(); view.installHandlers();
}); });
......
...@@ -12,11 +12,11 @@ describe("OpenAssessment.PeerView", function() { ...@@ -12,11 +12,11 @@ describe("OpenAssessment.PeerView", function() {
} }
).promise(); ).promise();
this.peerAssess = function(optionsSelected, feedback) { this.peerAssess = function() {
return successPromise; return successPromise;
}; };
this.render = function(step) { this.render = function() {
return successPromise; return successPromise;
}; };
...@@ -25,40 +25,28 @@ describe("OpenAssessment.PeerView", function() { ...@@ -25,40 +25,28 @@ describe("OpenAssessment.PeerView", function() {
}; };
}; };
// Stub base view
var StubBaseView = function() {
this.showLoadError = function(msg) {};
this.toggleActionError = function(msg, step) {};
this.setUpCollapseExpand = function(sel) {};
this.scrollToTop = function() {};
this.loadAssessmentModules = function() {};
this.loadMessageView = function() {};
};
// Stubs // Stubs
var baseView = null;
var server = null; var server = null;
var runtime = {};
// View under test var createPeerAssessmentView = function(template) {
var view = null; loadFixtures(template);
beforeEach(function() { var assessmentElement = $('#openassessment__peer-assessment').get(0);
// Load the DOM fixture var baseView = new OpenAssessment.BaseView(runtime, assessmentElement, server, {});
loadFixtures('oa_peer_assessment.html'); var view = new OpenAssessment.PeerView(assessmentElement, server, baseView);
view.installHandlers();
return view;
};
beforeEach(function() {
// Create a new stub server // Create a new stub server
server = new StubServer(); server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex');
// Create the stub base view
baseView = new StubBaseView();
// Create the object under test
var el = $("#openassessment-base").get(0);
view = new OpenAssessment.PeerView(el, server, baseView);
view.installHandlers();
}); });
it("Sends a peer assessment to the server", function() { it("Sends a peer assessment to the server", function() {
var view = createPeerAssessmentView('oa_peer_assessment.html');
spyOn(server, 'peerAssess').and.callThrough(); spyOn(server, 'peerAssess').and.callThrough();
// Select options in the rubric // Select options in the rubric
...@@ -89,6 +77,7 @@ describe("OpenAssessment.PeerView", function() { ...@@ -89,6 +77,7 @@ describe("OpenAssessment.PeerView", function() {
}); });
it("Re-enables the peer assess button on error", function() { it("Re-enables the peer assess button on error", function() {
var view = createPeerAssessmentView('oa_peer_assessment.html');
// Simulate a server error // Simulate a server error
spyOn(server, 'peerAssess').and.callFake(function() { spyOn(server, 'peerAssess').and.callFake(function() {
expect(view.peerSubmitEnabled()).toBe(false); expect(view.peerSubmitEnabled()).toBe(false);
...@@ -103,8 +92,8 @@ describe("OpenAssessment.PeerView", function() { ...@@ -103,8 +92,8 @@ describe("OpenAssessment.PeerView", function() {
}); });
it("Re-enables the continued grading button on error", function() { it("Re-enables the continued grading button on error", function() {
jasmine.getFixtures().fixturesPath = 'base/fixtures'; var view = createPeerAssessmentView('oa_peer_complete.html');
loadFixtures('oa_peer_complete.html');
// Simulate a server error // Simulate a server error
spyOn(server, 'renderContinuedPeer').and.callFake(function() { spyOn(server, 'renderContinuedPeer').and.callFake(function() {
expect(view.continueAssessmentEnabled()).toBe(false); expect(view.continueAssessmentEnabled()).toBe(false);
......
...@@ -9,7 +9,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -9,7 +9,7 @@ describe("OpenAssessment.ResponseView", function() {
'image/gif', 'image/gif',
'image/jpeg', 'image/jpeg',
'image/pjpeg', 'image/pjpeg',
'image/png', 'image/png'
]; ];
var ALLOWED_FILE_MIME_TYPES = [ var ALLOWED_FILE_MIME_TYPES = [
...@@ -17,7 +17,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -17,7 +17,7 @@ describe("OpenAssessment.ResponseView", function() {
'image/gif', 'image/gif',
'image/jpeg', 'image/jpeg',
'image/pjpeg', 'image/pjpeg',
'image/png', 'image/png'
]; ];
var FILE_TYPE_WHITE_LIST = ['pdf', 'doc', 'docx', 'html']; var FILE_TYPE_WHITE_LIST = ['pdf', 'doc', 'docx', 'html'];
...@@ -71,7 +71,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -71,7 +71,7 @@ describe("OpenAssessment.ResponseView", function() {
// Store the args we were passed so we can verify them // Store the args we were passed so we can verify them
this.uploadArgs = { this.uploadArgs = {
url: url, url: url,
data: data, data: data
}; };
// Return a promise indicating success or error // Return a promise indicating success or error
...@@ -79,18 +79,9 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -79,18 +79,9 @@ describe("OpenAssessment.ResponseView", function() {
}; };
}; };
var StubBaseView = function() {
this.loadAssessmentModules = function() {};
this.peerView = { load: function() {} };
this.gradeView = { load: function() {} };
this.showLoadError = function() {};
this.toggleActionError = function() {};
this.setUpCollapseExpand = function() {};
};
// Stubs // Stubs
var baseView = null;
var server = null; var server = null;
var runtime = {};
var fileUploader = null; var fileUploader = null;
var data = null; var data = null;
...@@ -120,7 +111,6 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -120,7 +111,6 @@ describe("OpenAssessment.ResponseView", function() {
server = new StubServer(); server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex'); server.renderLatex = jasmine.createSpy('renderLatex');
fileUploader = new StubFileUploader(); fileUploader = new StubFileUploader();
baseView = new StubBaseView();
data = { data = {
"ALLOWED_IMAGE_MIME_TYPES": ALLOWED_IMAGE_MIME_TYPES, "ALLOWED_IMAGE_MIME_TYPES": ALLOWED_IMAGE_MIME_TYPES,
"ALLOWED_FILE_MIME_TYPES": ALLOWED_FILE_MIME_TYPES, "ALLOWED_FILE_MIME_TYPES": ALLOWED_FILE_MIME_TYPES,
...@@ -129,8 +119,9 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -129,8 +119,9 @@ describe("OpenAssessment.ResponseView", function() {
}; };
// Create and install the view // Create and install the view
var el = $('#openassessment-base').get(0); var responseElement = $('#openassessment__response').get(0);
view = new OpenAssessment.ResponseView(el, server, fileUploader, baseView, data); var baseView = new OpenAssessment.BaseView(runtime, responseElement, server, {});
view = new OpenAssessment.ResponseView(responseElement, server, fileUploader, baseView, data);
view.installHandlers(); view.installHandlers();
// Stub the confirmation step // Stub the confirmation step
...@@ -296,14 +287,14 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -296,14 +287,14 @@ describe("OpenAssessment.ResponseView", function() {
}).promise(); }).promise();
}); });
spyOn(view, 'load'); spyOn(view, 'load');
spyOn(baseView, 'loadAssessmentModules'); spyOn(view.baseView, 'loadAssessmentModules');
view.response(['Test response 1', 'Test response 2']); view.response(['Test response 1', 'Test response 2']);
view.submit(); view.submit();
// Expect the current and next step to have been reloaded // Expect the current and next step to have been reloaded
expect(view.load).toHaveBeenCalled(); expect(view.load).toHaveBeenCalled();
expect(baseView.loadAssessmentModules).toHaveBeenCalled(); expect(view.baseView.loadAssessmentModules).toHaveBeenCalled();
}); });
it("enables the unsaved work warning when the user changes the response text", function() { it("enables the unsaved work warning when the user changes the response text", function() {
...@@ -442,39 +433,45 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -442,39 +433,45 @@ describe("OpenAssessment.ResponseView", function() {
}); });
it("selects too large of a file", function() { it("selects too large of a file", function() {
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpeg', size: 6000000, name: 'huge-picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 6000000, name: 'huge-picture.jpg', data: ''}];
view.prepareUpload(files, 'image'); view.prepareUpload(files, 'image');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File size must be 5MB or less.'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File size must be 5MB or less.');
}); });
it("selects the wrong image file type", function() { it("selects the wrong image file type", function() {
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpg', size: 1024, name: 'picture.exe', data: ''}]; var files = [{type: 'image/jpg', size: 1024, name: 'picture.exe', data: ''}];
view.prepareUpload(files, 'image'); view.prepareUpload(files, 'image');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'You can upload files with these file types: JPG, PNG or GIF'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith(
'upload', 'You can upload files with these file types: JPG, PNG or GIF'
);
}); });
it("selects the wrong pdf or image file type", function() { it("selects the wrong pdf or image file type", function() {
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}]; var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
view.prepareUpload(files, 'pdf-and-image'); view.prepareUpload(files, 'pdf-and-image');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'You can upload files with these file types: JPG, PNG, GIF or PDF'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith(
'upload', 'You can upload files with these file types: JPG, PNG, GIF or PDF'
);
}); });
it("selects the wrong file extension", function() { it("selects the wrong file extension", function() {
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}]; var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
view.prepareUpload(files, 'custom'); view.prepareUpload(files, 'custom');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'You can upload files with these file types: pdf, doc, docx, html'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith(
'upload', 'You can upload files with these file types: pdf, doc, docx, html'
);
}); });
it("submits a file with extension in the black list", function() { it("submits a file with extension in the black list", function() {
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(view.baseView, 'toggleActionError').and.callThrough();
view.data.FILE_TYPE_WHITE_LIST = ['exe']; view.data.FILE_TYPE_WHITE_LIST = ['exe'];
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}]; var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
view.prepareUpload(files, 'custom'); view.prepareUpload(files, 'custom');
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File type is not allowed.'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'File type is not allowed.');
}); });
it("uploads an image using a one-time URL", function() { it("uploads an image using a one-time URL", function() {
...@@ -504,7 +501,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -504,7 +501,7 @@ describe("OpenAssessment.ResponseView", function() {
it("displays an error if a one-time file upload URL cannot be retrieved", function() { it("displays an error if a one-time file upload URL cannot be retrieved", function() {
// Configure the server to fail when retrieving the one-time URL // Configure the server to fail when retrieving the one-time URL
server.uploadUrlError = true; server.uploadUrlError = true;
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(view.baseView, 'toggleActionError').and.callThrough();
// Attempt to upload a file // Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
...@@ -512,13 +509,13 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -512,13 +509,13 @@ describe("OpenAssessment.ResponseView", function() {
view.fileUpload(); view.fileUpload();
// Expect an error to be displayed // Expect an error to be displayed
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
}); });
it("displays an error if a file could not be uploaded", function() { it("displays an error if a file could not be uploaded", function() {
// Configure the file upload server to return an error // Configure the file upload server to return an error
fileUploader.uploadError = true; fileUploader.uploadError = true;
spyOn(baseView, 'toggleActionError').and.callThrough(); spyOn(view.baseView, 'toggleActionError').and.callThrough();
// Attempt to upload a file // Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}]; var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
...@@ -526,6 +523,6 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -526,6 +523,6 @@ describe("OpenAssessment.ResponseView", function() {
view.fileUpload(); view.fileUpload();
// Expect an error to be displayed // Expect an error to be displayed
expect(baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR'); expect(view.baseView.toggleActionError).toHaveBeenCalledWith('upload', 'ERROR');
}); });
}); });
...@@ -21,17 +21,8 @@ describe("OpenAssessment.SelfView", function() { ...@@ -21,17 +21,8 @@ describe("OpenAssessment.SelfView", function() {
}; };
}; };
// Stub base view
var StubBaseView = function() {
this.showLoadError = function(msg) {};
this.toggleActionError = function(msg, step) {};
this.setUpCollapseExpand = function(sel) {};
this.loadAssessmentModules = function() {};
this.scrollToTop = function() {};
};
// Stubs // Stubs
var baseView = null; var runtime = {};
var server = null; var server = null;
// View under test // View under test
...@@ -43,13 +34,12 @@ describe("OpenAssessment.SelfView", function() { ...@@ -43,13 +34,12 @@ describe("OpenAssessment.SelfView", function() {
// Create a new stub server // Create a new stub server
server = new StubServer(); server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex');
// Create the stub base view
baseView = new StubBaseView();
// Create the object under test // Create the object under test
var el = $("#openassessment").get(0); var assessmentElement = $("#openassessment__self-assessment").get(0);
view = new OpenAssessment.SelfView(el, server, baseView); var baseView = new OpenAssessment.BaseView(runtime, assessmentElement, server, {});
view = new OpenAssessment.SelfView(assessmentElement, server, baseView);
view.installHandlers(); view.installHandlers();
}); });
......
...@@ -20,6 +20,14 @@ describe('OpenAssessment.StaffAreaView', function() { ...@@ -20,6 +20,14 @@ describe('OpenAssessment.StaffAreaView', function() {
}); });
}; };
this.studentInfo = function() {
var server = this;
return $.Deferred(function(defer) {
var fragment = readFixtures('oa_student_info.html');
defer.resolveWith(server, [fragment]);
});
};
this.scheduleTraining = function() { this.scheduleTraining = function() {
var server = this; var server = this;
return $.Deferred(function(defer) { return $.Deferred(function(defer) {
...@@ -46,19 +54,9 @@ describe('OpenAssessment.StaffAreaView', function() { ...@@ -46,19 +54,9 @@ describe('OpenAssessment.StaffAreaView', function() {
}; };
// Stub base view
var StubBaseView = function() {
this.showLoadError = function() {};
this.toggleActionError = function() {};
this.setUpCollapseExpand = function() {};
this.scrollToTop = function() {};
this.loadAssessmentModules = function() {};
this.loadMessageView = function() {};
};
// Stubs // Stubs
var baseView = null;
var server = null; var server = null;
var runtime = {};
/** /**
* Create a staff area view. * Create a staff area view.
...@@ -69,8 +67,9 @@ describe('OpenAssessment.StaffAreaView', function() { ...@@ -69,8 +67,9 @@ describe('OpenAssessment.StaffAreaView', function() {
if (serverResponse) { if (serverResponse) {
server.data = serverResponse; server.data = serverResponse;
} }
var el = $('#openassessment').get(0); var assessmentElement = $('#openassessment').get(0);
var view = new OpenAssessment.StaffAreaView(el, server, baseView); var baseView = new OpenAssessment.BaseView(runtime, assessmentElement, server, {});
var view = new OpenAssessment.StaffAreaView(assessmentElement, server, baseView);
view.load(); view.load();
return view; return view;
}; };
...@@ -93,8 +92,6 @@ describe('OpenAssessment.StaffAreaView', function() { ...@@ -93,8 +92,6 @@ describe('OpenAssessment.StaffAreaView', function() {
// Create a new stub server // Create a new stub server
server = new StubServer(); server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex'); server.renderLatex = jasmine.createSpy('renderLatex');
// Create the stub base view
baseView = new StubBaseView();
}); });
describe('Initial rendering', function() { describe('Initial rendering', function() {
...@@ -165,46 +162,64 @@ describe('OpenAssessment.StaffAreaView', function() { ...@@ -165,46 +162,64 @@ describe('OpenAssessment.StaffAreaView', function() {
}); });
}); });
describe('Submission Management', function() { describe('Student Info', function() {
it('updates submission cancellation button when comments changes', function() { var chooseStudent = function(view, studentName) {
// Prevent the server's response from resolving, var studentNameField = $('.openassessment__student_username', view.element),
// so we can see what happens before view gets re-rendered. submitButton = $('.action--submit-username', view.element);
spyOn(server, 'cancelSubmission').and.callFake(function() { studentNameField.val(studentName);
return $.Deferred(function() {}).promise(); submitButton.click();
}); };
// Load the fixture
loadFixtures('oa_student_info.html');
var view = createStaffArea();
// comments is blank --> cancel submission button disabled
view.comment('');
view.handleCommentChanged();
expect(view.cancelSubmissionEnabled()).toBe(false);
// Response is whitespace --> cancel submission button disabled beforeEach(function() {
view.comment(' \n \n '); loadFixtures('oa_base_course_staff.html');
view.handleCommentChanged(); appendLoadFixtures('oa_student_info.html');
expect(view.cancelSubmissionEnabled()).toBe(false); });
// Response is not blank --> cancel submission button enabled it('shows an error when clicking "Submit" with no student name chosen', function() {
view.comment('Cancellation reason.'); var staffArea = createStaffArea();
view.handleCommentChanged(); chooseStudent(staffArea, '');
expect(view.cancelSubmissionEnabled()).toBe(true); expect($('.openassessment_student_info_form .form--error', staffArea.element).text().trim())
.toBe('A learner name must be provided.');
}); });
it('submits the cancel submission comments to the server', function() { describe('Submission Management', function() {
spyOn(server, 'cancelSubmission').and.callThrough(); it('updates submission cancellation button when comments changes', function() {
// Prevent the server's response from resolving,
// so we can see what happens before view gets re-rendered.
spyOn(server, 'cancelSubmission').and.callFake(function() {
return $.Deferred(function() {}).promise();
});
var staffArea = createStaffArea();
chooseStudent(staffArea, 'testStudent');
// comments is blank --> cancel submission button disabled
staffArea.comment('');
staffArea.handleCommentChanged();
expect(staffArea.cancelSubmissionEnabled()).toBe(false);
// Response is whitespace --> cancel submission button disabled
staffArea.comment(' \n \n ');
staffArea.handleCommentChanged();
expect(staffArea.cancelSubmissionEnabled()).toBe(false);
// Response is not blank --> cancel submission button enabled
staffArea.comment('Cancellation reason.');
staffArea.handleCommentChanged();
expect(staffArea.cancelSubmissionEnabled()).toBe(true);
});
// Load the fixture it('submits the cancel submission comments to the server', function() {
loadFixtures('oa_student_info.html'); spyOn(server, 'cancelSubmission').and.callThrough();
var view = createStaffArea();
view.comment('Cancellation reason.'); var staffArea = createStaffArea();
view.cancelSubmission('Bob'); chooseStudent(staffArea, 'testStudent');
expect(server.cancelSubmission).toHaveBeenCalledWith('Bob', 'Cancellation reason.'); staffArea.comment('Cancellation reason.');
staffArea.cancelSubmission('Bob');
expect(server.cancelSubmission).toHaveBeenCalledWith('Bob', 'Cancellation reason.');
});
}); });
}); });
......
...@@ -27,18 +27,9 @@ describe("OpenAssessment.StudentTrainingView", function() { ...@@ -27,18 +27,9 @@ describe("OpenAssessment.StudentTrainingView", function() {
this.corrections = {}; this.corrections = {};
}; };
// Stub base view
var StubBaseView = function() {
this.showLoadError = function(msg) {};
this.toggleActionError = function(msg, step) {};
this.setUpCollapseExpand = function(sel) {};
this.scrollToTop = function() {};
this.loadAssessmentModules = function() {};
};
// Stubs // Stubs
var baseView = null;
var server = null; var server = null;
var runtime = {};
// View under test // View under test
var view = null; var view = null;
...@@ -50,12 +41,11 @@ describe("OpenAssessment.StudentTrainingView", function() { ...@@ -50,12 +41,11 @@ describe("OpenAssessment.StudentTrainingView", function() {
// Create a new stub server // Create a new stub server
server = new StubServer(); server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex') server.renderLatex = jasmine.createSpy('renderLatex')
// Create the stub base view
baseView = new StubBaseView();
// Create the object under test // Create the object under test
var el = $("#openassessment-base").get(0); var trainingElement = $('#openassessment__student-training').get(0);
view = new OpenAssessment.StudentTrainingView(el, server, baseView); var baseView = new OpenAssessment.BaseView(runtime, trainingElement, server, {});
view = new OpenAssessment.StudentTrainingView(trainingElement, server, baseView);
view.installHandlers(); view.installHandlers();
}); });
...@@ -112,7 +102,7 @@ describe("OpenAssessment.StudentTrainingView", function() { ...@@ -112,7 +102,7 @@ describe("OpenAssessment.StudentTrainingView", function() {
// Simulate that the user answered the problem correctly, so there are no corrections // Simulate that the user answered the problem correctly, so there are no corrections
server.corrections = {}; server.corrections = {};
spyOn(server, 'trainingAssess').and.callThrough(); spyOn(server, 'trainingAssess').and.callThrough();
spyOn(baseView, 'loadAssessmentModules').and.callThrough(); spyOn(view.baseView, 'loadAssessmentModules').and.callThrough();
// Select rubric options // Select rubric options
var optionsSelected = {}; var optionsSelected = {};
...@@ -128,6 +118,6 @@ describe("OpenAssessment.StudentTrainingView", function() { ...@@ -128,6 +118,6 @@ describe("OpenAssessment.StudentTrainingView", function() {
expect(server.trainingAssess).toHaveBeenCalledWith(optionsSelected); expect(server.trainingAssess).toHaveBeenCalledWith(optionsSelected);
// Expect that the steps were reloaded // Expect that the steps were reloaded
expect(baseView.loadAssessmentModules).toHaveBeenCalled(); expect(view.baseView.loadAssessmentModules).toHaveBeenCalled();
}); });
}); });
...@@ -4,3 +4,4 @@ Common test configuration, loaded before any of the spec files. ...@@ -4,3 +4,4 @@ Common test configuration, loaded before any of the spec files.
// Set the fixture path // Set the fixture path
jasmine.getFixtures().fixturesPath = 'base/fixtures'; jasmine.getFixtures().fixturesPath = 'base/fixtures';
...@@ -43,13 +43,12 @@ OpenAssessment.BaseView.prototype = { ...@@ -43,13 +43,12 @@ OpenAssessment.BaseView.prototype = {
}, },
/** /**
Install click handlers to expand/collapse a section. * Install click handlers to expand/collapse a section.
*
Args: * @param {element} parentElement JQuery selector for the container element.
parentSel (JQuery selector): CSS selector for the container element. */
**/ setUpCollapseExpand: function(parentElement) {
setUpCollapseExpand: function(parentSel) { parentElement.on('click', '.ui-toggle-visibility__control', function(eventData) {
parentSel.on('click', '.ui-toggle-visibility__control', function(eventData) {
var sel = $(eventData.target).closest('.ui-toggle-visibility'); var sel = $(eventData.target).closest('.ui-toggle-visibility');
sel.toggleClass('is--collapsed'); sel.toggleClass('is--collapsed');
} }
...@@ -57,8 +56,8 @@ OpenAssessment.BaseView.prototype = { ...@@ -57,8 +56,8 @@ OpenAssessment.BaseView.prototype = {
}, },
/** /**
Asynchronously load each sub-view into the DOM. * Asynchronously load each sub-view into the DOM.
**/ */
load: function() { load: function() {
this.responseView.load(); this.responseView.load();
this.loadAssessmentModules(); this.loadAssessmentModules();
...@@ -66,9 +65,9 @@ OpenAssessment.BaseView.prototype = { ...@@ -66,9 +65,9 @@ OpenAssessment.BaseView.prototype = {
}, },
/** /**
Refresh the Assessment Modules. This should be called any time an action is * Refresh the Assessment Modules. This should be called any time an action is
performed by the user. * performed by the user.
**/ */
loadAssessmentModules: function() { loadAssessmentModules: function() {
this.trainingView.load(); this.trainingView.load();
this.peerView.load(); this.peerView.load();
...@@ -91,21 +90,20 @@ OpenAssessment.BaseView.prototype = { ...@@ -91,21 +90,20 @@ OpenAssessment.BaseView.prototype = {
}, },
/** /**
Refresh the message only (called by PeerView to update and avoid race condition) * Refresh the message only (called by PeerView to update and avoid race condition)
**/ */
loadMessageView: function() { loadMessageView: function() {
this.messageView.load(); this.messageView.load();
}, },
/** /**
Report an error to the user. * Report an error to the user.
*
Args: * @param {string} type The type of error. Options are "save", submit", "peer", and "self".
type (str): Which type of error. Options are "save", submit", "peer", and "self". * @param {string} message The error message to display, or if null hide the message.
msg (str or null): The error message to display. * Note: loading errors are never hidden once displayed.
If null, hide the error message (with one exception: loading errors are never hidden once displayed) */
**/ toggleActionError: function(type, message) {
toggleActionError: function(type, msg) {
var element = this.element; var element = this.element;
var container = null; var container = null;
if (type === 'save') { if (type === 'save') {
...@@ -123,29 +121,32 @@ OpenAssessment.BaseView.prototype = { ...@@ -123,29 +121,32 @@ OpenAssessment.BaseView.prototype = {
// If we don't have anywhere to put the message, just log it to the console // If we don't have anywhere to put the message, just log it to the console
if (container === null) { if (container === null) {
if (msg !== null) { console.log(msg); } if (message !== null) { console.log(message); }
} }
else { else {
// Insert the error message // Insert the error message
var msgHtml = (msg === null) ? "" : msg; var msgHtml = (message === null) ? "" : message;
$(container + " .message__content", element).html('<p>' + msgHtml + '</p>'); $(container + " .message__content", element).html('<p>' + msgHtml + '</p>');
// Toggle the error class // Toggle the error class
$(container, element).toggleClass('has--error', msg !== null); $(container, element).toggleClass('has--error', message !== null);
} }
}, },
/** /**
Report an error loading a step. * Report an error loading a step.
*
Args: * @param {string} stepName The step that could not be loaded.
step (str): the step that could not be loaded. * @param {string} errorMessage An optional error message to use instead of the default.
**/ */
showLoadError: function(step) { showLoadError: function(stepName, errorMessage) {
var container = '#openassessment__' + step; if (!errorMessage) {
$(container).toggleClass('has--error', true); errorMessage = gettext('Unable to load');
$(container + ' .step__status__value i').removeClass().addClass('icon fa fa-exclamation-triangle'); }
$(container + ' .step__status__value .copy').html(gettext('Unable to Load')); var $container = $('#openassessment__' + stepName);
$container.toggleClass('has--error', true);
$container.find('.step__status__value i').removeClass().addClass('icon fa fa-exclamation-triangle');
$container.find('.step__status__value .copy').html(errorMessage);
} }
}; };
......
...@@ -35,6 +35,16 @@ ...@@ -35,6 +35,16 @@
.ui-staff__content { .ui-staff__content {
margin-top: 0; margin-top: 0;
} }
.staff-info__cancel-submission__content,
.staff-info__staff-override__content {
padding: 0;
}
}
.ui-toggle-visibility__content {
@include margin-left(($baseline-h/4));
margin-bottom: ($baseline-v/2);
} }
} }
} }
...@@ -44,6 +54,21 @@ ...@@ -44,6 +54,21 @@
color: $copy-staff-color; color: $copy-staff-color;
} }
.ui-staff__subtitle {
@extend %t-subheading;
@extend %t-strong;
@include fontSize($f-size-medium);
// We want to keep the collapsible headers within the staff assessment block blue
// (because they are being displayed in the LMS color scheme). Unfortunately because of
// that we need to add an override just for ui-staff_subtitle collapsible items.
color: $heading-staff-color !important;
margin-bottom: ($baseline-v/2);
span {
font-weight: inherit;
}
}
.staff-info__title__copy { .staff-info__title__copy {
@extend %t-strong; @extend %t-strong;
} }
...@@ -54,10 +79,7 @@ ...@@ -54,10 +79,7 @@
} }
.ui-staff__content__section { .ui-staff__content__section {
padding-bottom: $baseline-v; padding-bottom: ($baseline-v/2);
border-bottom: 1px solid rgba($color-decorative-staff, 0.25);
margin-bottom: $baseline-v;
@extend %wipe-last-child; @extend %wipe-last-child;
} }
...@@ -110,7 +132,7 @@ ...@@ -110,7 +132,7 @@
} }
th, td { th, td {
border: 1px solid rgba($color-decorative-staff, 0.25); border: 1px solid rgba($copy-staff-color, 0.25);
padding: ($baseline-v/2) ($baseline-h/4); padding: ($baseline-v/2) ($baseline-h/4);
} }
...@@ -136,7 +158,7 @@ ...@@ -136,7 +158,7 @@
} }
// UI - cancel submission (action) // UI - cancel submission (action)
.openassessment__staff-info__cancel__submission { .staff-info__workflow-cancellation {
.staff-info__cancel-submission__content { .staff-info__cancel-submission__content {
......
...@@ -110,10 +110,15 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or ...@@ -110,10 +110,15 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
@include margin(($baseline-v/2), ($baseline-v/2), ($baseline-v/2), ($baseline-v/2)); @include margin(($baseline-v/2), ($baseline-v/2), ($baseline-v/2), ($baseline-v/2));
} }
} }
.staff-info__student { .staff-info__student {
.label { .label {
color: $heading-staff-color; color: $heading-staff-color;
margin-bottom: ($baseline-v/2); margin-bottom: ($baseline-v/2);
input {
display: block;
}
} }
.action--submit { .action--submit {
...@@ -129,33 +134,115 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or ...@@ -129,33 +134,115 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
} }
.title--sub { .title--sub {
@extend %hd-4;
color: $heading-staff-color; color: $heading-staff-color;
margin-top: ($baseline-v/2); margin-top: ($baseline-v/2);
margin-bottom: ($baseline-v/2); margin-bottom: ($baseline-v/2);
} }
.student__answer__display__content { .student__answer__display__content {
border: 1px solid rgba($color-decorative-staff, 0.25); border: 1px solid rgba($copy-staff-color, 0.25);
@include padding(($baseline-v/2), ($baseline-h/2), ($baseline-v/2), ($baseline-h/2)); padding: ($baseline-v/2) ($baseline-h/4);
margin-bottom: ($baseline-v/2); margin-bottom: ($baseline-v/2);
} }
.openassessment__student-info_list { .staff-info__student__report {
list-style-type: none; list-style-type: none;
.title {
@extend %t-strong;
margin-top: ($baseline-v/2);
border-top: 1px solid $heading-staff-color;
padding: ($baseline-v/2) ($baseline-h/2) ($baseline-v/2) 0;
span {
font-weight: inherit;
}
}
}
.staff-info__cancel-submission__content,
.staff-info__staff-override__content {
padding: $baseline-v ($baseline-h/2);
background-color: white;
} }
.value { .value {
width: $max-width/2; width: $max-width/2;
} }
/** // staff assessments
* The follow styles are bound for the "shame" file. This is done to override .wrapper--staff-assessment {
* LMS specific styles on HTML elements. 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;
}
// 'p' elements in LMS have a color set on them. // rubric options
.student__answer__display__content p { .question__answers {
color: inherit; @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);
}
}
} }
} }
...@@ -1144,6 +1231,8 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or ...@@ -1144,6 +1231,8 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
.self-assessment__display__title, .self-assessment__display__title,
.peer-assessment__display__header .peer-assessment__display__header
.peer-assessment__display__title, .peer-assessment__display__title,
.staff-assessment__display__header
.staff-assessment__display__title,
.submission__answer__display .submission__answer__display
.submission__answer__display__title{ .submission__answer__display__title{
margin: 10px 0; margin: 10px 0;
...@@ -1152,6 +1241,7 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or ...@@ -1152,6 +1241,7 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
.self-assessment__display__image, .self-assessment__display__image,
.peer-assessment__display__image, .peer-assessment__display__image,
.staff-assessment__display__image,
.submission__answer__display__image{ .submission__answer__display__image{
@extend .submission__answer__display__content; @extend .submission__answer__display__content;
max-height: 400px; max-height: 400px;
......
...@@ -193,7 +193,14 @@ ...@@ -193,7 +193,14 @@
color: $copy-staff-color !important; color: $copy-staff-color !important;
} }
.openassessment__staff-info__cancel__submission { .staff-info__workflow-cancellation {
margin-bottom: ($baseline-v) !important; 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;
}
}
...@@ -110,21 +110,27 @@ class WorkflowMixin(object): ...@@ -110,21 +110,27 @@ class WorkflowMixin(object):
requirements = self.workflow_requirements() requirements = self.workflow_requirements()
workflow_api.update_from_assessments(submission_uuid, requirements) workflow_api.update_from_assessments(submission_uuid, requirements)
def get_workflow_info(self): def get_workflow_info(self, submission_uuid=None):
""" """
Retrieve a description of the student's progress in a workflow. Retrieve a description of the student's progress in a workflow.
Note that this *may* update the workflow status if it's changed. Note that this *may* update the workflow status if it's changed.
Keyword Arguments:
submission_uuid (str): The submission associated with the workflow to return.
Defaults to the submission created by the current student.
Returns: Returns:
dict dict
Raises: Raises:
AssessmentWorkflowError AssessmentWorkflowError
""" """
if not self.submission_uuid: if not submission_uuid:
return {} submission_uuid = self.submission_uuid
if not submission_uuid:
return {}
return workflow_api.get_workflow_for_submission( return workflow_api.get_workflow_for_submission(
self.submission_uuid, self.workflow_requirements() submission_uuid, self.workflow_requirements()
) )
def get_workflow_status_counts(self): def get_workflow_status_counts(self):
......
...@@ -25,7 +25,6 @@ class OpenAssessmentA11yTest(OpenAssessmentTest): ...@@ -25,7 +25,6 @@ class OpenAssessmentA11yTest(OpenAssessmentTest):
) )
page.a11y_audit.config.set_rules({ page.a11y_audit.config.set_rules({
"ignore": [ "ignore": [
"aria-valid-attr", # TODO: AC-199
"color-contrast", # TODO: AC-198 "color-contrast", # TODO: AC-198
"empty-heading", # TODO: AC-197 "empty-heading", # TODO: AC-197
"link-href", # TODO: AC-199 "link-href", # TODO: AC-199
...@@ -77,12 +76,14 @@ class StudentTrainingA11yTest(OpenAssessmentA11yTest): ...@@ -77,12 +76,14 @@ class StudentTrainingA11yTest(OpenAssessmentA11yTest):
class StaffAreaA11yTest(OpenAssessmentA11yTest): class StaffAreaA11yTest(OpenAssessmentA11yTest):
""" """
Test the accessibility of the staff area. Test the accessibility of the staff area.
This is testing a problem with "self assessment only".
""" """
def setUp(self): def setUp(self):
super(StaffAreaA11yTest, self).setUp('peer_only', staff=True) super(StaffAreaA11yTest, self).setUp('self_only', staff=True)
self.staff_area_page = StaffAreaPage(self.browser, self.problem_loc) self.staff_area_page = StaffAreaPage(self.browser, self.problem_loc)
def test_staff_tools_panel_a11y(self): def test_staff_tools_panel(self):
""" """
Check the accessibility of the "Staff Tools" panel Check the accessibility of the "Staff Tools" panel
""" """
...@@ -90,7 +91,7 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest): ...@@ -90,7 +91,7 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest):
self.staff_area_page.click_staff_toolbar_button("staff-tools") self.staff_area_page.click_staff_toolbar_button("staff-tools")
self._check_a11y(self.staff_area_page) self._check_a11y(self.staff_area_page)
def test_staff_info_panel_a11y(self): def test_staff_info_panel(self):
""" """
Check the accessibility of the "Staff Info" panel Check the accessibility of the "Staff Info" panel
""" """
...@@ -98,6 +99,20 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest): ...@@ -98,6 +99,20 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest):
self.staff_area_page.click_staff_toolbar_button("staff-info") self.staff_area_page.click_staff_toolbar_button("staff-info")
self._check_a11y(self.staff_area_page) 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.staff_area_page.visit()
# Click on staff tools and search for the user.
self.staff_area_page.show_learner(username)
self._check_a11y(self.staff_area_page)
if __name__ == "__main__": if __name__ == "__main__":
......
...@@ -79,3 +79,14 @@ class AutoAuthPage(PageObject): ...@@ -79,3 +79,14 @@ class AutoAuthPage(PageObject):
message = self.q(css='BODY').text[0].strip() message = self.q(css='BODY').text[0].strip()
match = re.search(r' user_id ([^$]+)$', message) match = re.search(r' user_id ([^$]+)$', message)
return match.groups()[0] if match else None return match.groups()[0] if match else None
def get_username(self):
"""
Finds and returns the username
"""
message = self.q(css='BODY').text[0].strip()
match = re.search(r'Logged in user ([^$]+) with password ([^$]+) and user_id ([^$]+)$', message)
if not match:
return None
username_and_email = match.groups()[0]
return username_and_email.split(' ')[0]
...@@ -343,11 +343,11 @@ class GradePage(OpenAssessmentPage): ...@@ -343,11 +343,11 @@ class GradePage(OpenAssessmentPage):
class StaffAreaPage(OpenAssessmentPage): class StaffAreaPage(OpenAssessmentPage):
""" """
Page object representing the "submission" step in an ORA problem. Page object representing the tabbed staff area.
""" """
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css="#openassessment__staff-area").is_present() return self.q(css=".openassessment__staff-area").is_present()
@property @property
def selected_button_names(self): def selected_button_names(self):
...@@ -360,10 +360,10 @@ class StaffAreaPage(OpenAssessmentPage): ...@@ -360,10 +360,10 @@ class StaffAreaPage(OpenAssessmentPage):
@property @property
def visible_staff_panels(self): def visible_staff_panels(self):
""" """
Returns the ids of the visible staff panels Returns the classes of the visible staff panels
""" """
panels = self.q(css=".wrapper--ui-staff") panels = self.q(css=".wrapper--ui-staff")
return [panel.get_attribute('id') for panel in panels if u'is--hidden' not in panel.get_attribute('class')] return [panel.get_attribute('class') for panel in panels if u'is--hidden' not in panel.get_attribute('class')]
def click_staff_toolbar_button(self, button_name): def click_staff_toolbar_button(self, button_name):
""" """
...@@ -379,3 +379,29 @@ class StaffAreaPage(OpenAssessmentPage): ...@@ -379,3 +379,29 @@ class StaffAreaPage(OpenAssessmentPage):
:return: :return:
""" """
self.q(css=".wrapper--{panel_name} .ui-staff_close_button".format(panel_name=panel_name)).click() self.q(css=".wrapper--{panel_name} .ui-staff_close_button".format(panel_name=panel_name)).click()
def show_learner(self, username):
"""
Clicks the staff tools panel and and searches for learner information about the given username.
"""
self.click_staff_toolbar_button("staff-tools")
self.wait_for_element_visibility("input.openassessment__student_username", "Input is present")
self.q(css="input.openassessment__student_username").fill(username)
submit_button = self.q(css=".action--submit-username")
submit_button.first.click()
self.wait_for_element_visibility(".staff-info__student__report", "Student report is present")
@property
def learner_report_text(self):
"""
Returns the text present in the learner report (useful for case where there is no response).
"""
return self.q(css=".staff-info__student__report").text[0]
@property
def learner_report_sections(self):
"""
Returns the titles of the collapsible learner report sections present on the page.
"""
sections = self.q(css=".ui-staff__subtitle")
return [section.text for section in sections]
...@@ -90,6 +90,27 @@ class OpenAssessmentTest(WebAppTest): ...@@ -90,6 +90,27 @@ class OpenAssessmentTest(WebAppTest):
self.student_training_page = AssessmentPage('student-training', self.browser, self.problem_loc) self.student_training_page = AssessmentPage('student-training', self.browser, self.problem_loc)
self.grade_page = GradePage(self.browser, self.problem_loc) self.grade_page = GradePage(self.browser, self.problem_loc)
def do_self_assessment(self):
"""
Submits a self assessment, verifies the grade, and returns the username of the student
for which the self assessment was submitted.
"""
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 self-assessment
self.self_asmnt_page.wait_for_page().wait_for_response()
self.assertIn(self.SUBMISSION, self.self_asmnt_page.response_text)
self.self_asmnt_page.assess(self.OPTIONS_SELECTED).wait_for_complete()
self.assertTrue(self.self_asmnt_page.is_complete)
# Verify the grade
self.assertEqual(self.grade_page.wait_for_page().score, self.EXPECTED_SCORE)
return username
class SelfAssessmentTest(OpenAssessmentTest): class SelfAssessmentTest(OpenAssessmentTest):
""" """
...@@ -103,18 +124,7 @@ class SelfAssessmentTest(OpenAssessmentTest): ...@@ -103,18 +124,7 @@ class SelfAssessmentTest(OpenAssessmentTest):
@attr('acceptance') @attr('acceptance')
def test_self_assessment(self): def test_self_assessment(self):
# Submit a response # Submit a response
self.auto_auth_page.visit() self.do_self_assessment()
self.submission_page.visit().submit_response(self.SUBMISSION)
self.assertTrue(self.submission_page.has_submitted)
# Submit a self-assessment
self.self_asmnt_page.wait_for_page().wait_for_response()
self.assertIn(self.SUBMISSION, self.self_asmnt_page.response_text)
self.self_asmnt_page.assess(self.OPTIONS_SELECTED).wait_for_complete()
self.assertTrue(self.self_asmnt_page.is_complete)
# Verify the grade
self.assertEqual(self.grade_page.wait_for_page().score, self.EXPECTED_SCORE)
# Check browser scrolled back to top of assessment # Check browser scrolled back to top of assessment
self.assertTrue(self.self_asmnt_page.is_on_top) self.assertTrue(self.self_asmnt_page.is_on_top)
...@@ -216,10 +226,12 @@ class StudentTrainingTest(OpenAssessmentTest): ...@@ -216,10 +226,12 @@ class StudentTrainingTest(OpenAssessmentTest):
class StaffAreaTest(OpenAssessmentTest): class StaffAreaTest(OpenAssessmentTest):
""" """
Test the staff area. Test the staff area.
This is testing a problem with "self assessment only".
""" """
def setUp(self): def setUp(self):
super(StaffAreaTest, self).setUp('peer_only', staff=True) super(StaffAreaTest, self).setUp('self_only', staff=True)
self.staff_area_page = StaffAreaPage(self.browser, self.problem_loc) self.staff_area_page = StaffAreaPage(self.browser, self.problem_loc)
@retry() @retry()
...@@ -275,9 +287,9 @@ class StaffAreaTest(OpenAssessmentTest): ...@@ -275,9 +287,9 @@ class StaffAreaTest(OpenAssessmentTest):
# Click on the button and verify that the panel has opened # Click on the button and verify that the panel has opened
self.staff_area_page.click_staff_toolbar_button(panel_name) self.staff_area_page.click_staff_toolbar_button(panel_name)
self.assertEqual(self.staff_area_page.selected_button_names, [button_label]) self.assertEqual(self.staff_area_page.selected_button_names, [button_label])
self.assertEqual( self.assertIn(
self.staff_area_page.visible_staff_panels, u'openassessment__{button_name}'.format(button_name=panel_name),
[u'openassessment__{button_name}'.format(button_name=panel_name)] self.staff_area_page.visible_staff_panels[0]
) )
# Click 'Close' and verify that the panel has been closed # Click 'Close' and verify that the panel has been closed
...@@ -285,6 +297,51 @@ class StaffAreaTest(OpenAssessmentTest): ...@@ -285,6 +297,51 @@ class StaffAreaTest(OpenAssessmentTest):
self.assertEqual(self.staff_area_page.selected_button_names, []) self.assertEqual(self.staff_area_page.selected_button_names, [])
self.assertEqual(self.staff_area_page.visible_staff_panels, []) self.assertEqual(self.staff_area_page.visible_staff_panels, [])
@retry()
@attr('acceptance')
def test_student_info(self):
"""
Scenario: staff tools shows learner response information
Given I am viewing the staff area of an ORA problem
When I search for a learner in staff tools
And the learner has submitted a response to an ORA problem with self-assessment
Then I see the correct learner information sections
"""
username = self.do_self_assessment()
self.staff_area_page.visit()
# Click on staff tools and search for user
self.staff_area_page.show_learner(username)
self.assertNotIn('A response was not found for this learner', self.staff_area_page.learner_report_text)
self.assertEqual(
[u'Learner Response', u"Learner's Self Assessment", u"Learner's Final Grade"],
self.staff_area_page.learner_report_sections
)
@retry()
@attr('acceptance')
def test_student_info_no_submission(self):
"""
Scenario: staff tools indicates if no submission has been received for a given learner
Given I am viewing the staff area of an ORA problem
When I search for a learner in staff tools
And the learner has not submitted a response to the ORA problem
Then I see a message indicating that the learner has not submitted a response
And there are no student information sections displayed
"""
self.auto_auth_page.visit()
self.staff_area_page.visit()
# Click on staff tools and search for user
self.staff_area_page.show_learner('no-submission-learner')
self.assertIn('A response was not found for this learner', self.staff_area_page.learner_report_text)
self.assertEqual([], self.staff_area_page.learner_report_sections)
class FileUploadTest(OpenAssessmentTest): class FileUploadTest(OpenAssessmentTest):
""" """
......
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