Commit 5a530dab by Andy Armstrong

First phase of staff re-grading UI

parent bf30363c
......@@ -40,7 +40,7 @@
</ol>
{% if show_staff_area %}
<div id="openassessment__staff-area"></div>
<div class="openassessment__staff-area"></div>
{% endif %}
</div>
</div>
......
......@@ -10,7 +10,7 @@
>
<h4 class="question__title ui-toggle-visibility__control">
<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>
</h4>
......@@ -23,7 +23,8 @@
name="{{ criterion.name }}"
id="assessment__rubric__question--{{ criterion.order_num }}__{{ option.order_num }}"
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 }}"
class="answer__label"
>{{ option.label }}</label>
......
......@@ -139,10 +139,10 @@
<ul class="list list--actions">
<li class="list--actions__item">
<a aria-role="button" href="#" id="step--response__submit"
class="action action--submit step--response__submit {{ submit_enabled|yesno:",is--disabled" }}">
<button type="submit" id="step--response__submit"
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>
</a>
</button>
</li>
</ul>
</div>
......
{% load i18n %}
{% 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-info" data-panel="openassessment__staff-info">{% trans "Staff Info" %}</button>
</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">
<h2 class="staff-info__title ui-staff__title">
<span class="staff-info__title__copy">{% trans "Course Staff Tools" %}</span>
......@@ -18,17 +18,20 @@
<div class="staff-info__student ui-staff__content__section">
<div class="wrapper--input" class="staff-info__student__form">
<form id="openassessment_student_info_form">
<label for="openassessment__student_username" class="label">{% trans "Enter an individual learner's username or email" %}</label>
<input id="openassessment__student_username" type="text" class="value" maxlength="255">
<form class="openassessment_student_info_form">
<div class="form--error"></div>
<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">
<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>
</ul>
</form>
</div>
<div id="openassessment__student-info" class="staff-info__student__report"></div>
<div class="openassessment__student-info staff-info__student__report"></div>
</div>
{% if display_schedule_training %}
......@@ -65,22 +68,22 @@
</div>
<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>
<div id="schedule_training_message"></div>
<button class="action--submit action--submit-training"><span class="copy">{% trans "Schedule Example-Based Assessment Training" %}</span></button>
<div class="schedule_training_message"></div>
</div>
{% endif %}
{% if display_reschedule_unfinished_tasks %}
<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>
<div id="reschedule_unfinished_tasks_message"></div>
<button class="action--submit action--submit-unfinished-tasks"><span class="copy">{% trans "Reschedule All Unfinished Example-Based Assessment Grading Tasks" %}</span></button>
<div class="reschedule_unfinished_tasks_message"></div>
</div>
{% endif %}
</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">
<h2 class="staff-info__title ui-staff__title">
<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
from django.db import DatabaseError
from openassessment.assessment.api import peer as peer_api
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 openassessment.assessment.errors import PeerAssessmentError, PeerAssessmentInternalError
from submissions import api as sub_api
from .models import AssessmentWorkflow, AssessmentWorkflowCancellation, AssessmentWorkflowStep
from .models import AssessmentWorkflow, AssessmentWorkflowCancellation
from .serializers import AssessmentWorkflowSerializer, AssessmentWorkflowCancellationSerializer
from .errors import (
AssessmentWorkflowError, AssessmentWorkflowInternalError,
......
......@@ -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.workflow.models import AssessmentWorkflowCancellation
from openassessment.fileupload import exceptions as file_exceptions
......@@ -112,7 +113,7 @@ class StaffAreaMixin(object):
Gets the path and context for the staff section of the ORA XBlock.
"""
context = {}
path = 'openassessmentblock/staff_area/staff_area.html'
path = 'openassessmentblock/staff_area/oa_staff_area.html'
student_item = self.get_student_item_dict()
......@@ -214,16 +215,20 @@ class StaffAreaMixin(object):
"""
try:
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)
except PeerAssessmentInternalError:
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
section of the staff debug panel.
Get the proper path and context for rendering the student info
section of the staff area.
Args:
student_username (unicode): The username of the student to report.
......@@ -278,19 +283,31 @@ class StaffAreaMixin(object):
if "example-based-assessment" in assessment_steps:
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)
if workflow_cancellation:
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 = {
'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_cancelled_at': workflow_cancelled_at,
'peer_assessments': peer_assessments,
'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
'student_username': student_username,
'expanded_view': expanded_view,
}
if peer_assessments or self_assessment or example_based_assessment:
......@@ -298,7 +315,7 @@ class StaffAreaMixin(object):
for criterion in context["rubric_criteria"]:
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
@XBlock.json_handler
......
......@@ -651,7 +651,7 @@
"output": "oa_edit_student_training.html"
},
{
"template": "openassessmentblock/staff_area/staff_area.html",
"template": "openassessmentblock/staff_area/oa_staff_area.html",
"context": {
"status_counts": {
"self": 1,
......@@ -682,7 +682,7 @@
"output": "oa_staff_area.html"
},
{
"template": "openassessmentblock/staff_area/student_info.html",
"template": "openassessmentblock/staff_area/oa_student_info.html",
"context": {
"submission": {
"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() {
// Create a new stub server
server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex')
server.renderLatex = jasmine.createSpy('renderLatex');
// Create the object under test
var el = $("#openassessment").get(0);
view = new OpenAssessment.BaseView(runtime, el, server);
......
......@@ -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
var baseView = null;
var server = null;
var runtime = {};
// View under test
var view = null;
......@@ -47,12 +40,10 @@ describe("OpenAssessment.GradeView", function() {
// Create the stub server
server = new StubServer();
// Create the stub base view
baseView = new StubBaseView();
// Create and install the view
var el = $('#openassessment-base').get(0);
view = new OpenAssessment.GradeView(el, server, baseView);
var gradeElement = $('#openassessment__grade').get(0);
var baseView = new OpenAssessment.BaseView(runtime, gradeElement, server, {});
view = new OpenAssessment.GradeView(gradeElement, server, baseView);
view.installHandlers();
});
......
......@@ -12,11 +12,11 @@ describe("OpenAssessment.PeerView", function() {
}
).promise();
this.peerAssess = function(optionsSelected, feedback) {
this.peerAssess = function() {
return successPromise;
};
this.render = function(step) {
this.render = function() {
return successPromise;
};
......@@ -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
var baseView = null;
var server = null;
var runtime = {};
// View under test
var view = null;
var createPeerAssessmentView = function(template) {
loadFixtures(template);
beforeEach(function() {
// Load the DOM fixture
loadFixtures('oa_peer_assessment.html');
var assessmentElement = $('#openassessment__peer-assessment').get(0);
var baseView = new OpenAssessment.BaseView(runtime, assessmentElement, server, {});
var view = new OpenAssessment.PeerView(assessmentElement, server, baseView);
view.installHandlers();
return view;
};
beforeEach(function() {
// Create a new stub server
server = new StubServer();
// 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();
server.renderLatex = jasmine.createSpy('renderLatex');
});
it("Sends a peer assessment to the server", function() {
var view = createPeerAssessmentView('oa_peer_assessment.html');
spyOn(server, 'peerAssess').and.callThrough();
// Select options in the rubric
......@@ -89,6 +77,7 @@ describe("OpenAssessment.PeerView", function() {
});
it("Re-enables the peer assess button on error", function() {
var view = createPeerAssessmentView('oa_peer_assessment.html');
// Simulate a server error
spyOn(server, 'peerAssess').and.callFake(function() {
expect(view.peerSubmitEnabled()).toBe(false);
......@@ -103,8 +92,8 @@ describe("OpenAssessment.PeerView", function() {
});
it("Re-enables the continued grading button on error", function() {
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('oa_peer_complete.html');
var view = createPeerAssessmentView('oa_peer_complete.html');
// Simulate a server error
spyOn(server, 'renderContinuedPeer').and.callFake(function() {
expect(view.continueAssessmentEnabled()).toBe(false);
......
......@@ -9,7 +9,7 @@ describe("OpenAssessment.ResponseView", function() {
'image/gif',
'image/jpeg',
'image/pjpeg',
'image/png',
'image/png'
];
var ALLOWED_FILE_MIME_TYPES = [
......@@ -17,7 +17,7 @@ describe("OpenAssessment.ResponseView", function() {
'image/gif',
'image/jpeg',
'image/pjpeg',
'image/png',
'image/png'
];
var FILE_TYPE_WHITE_LIST = ['pdf', 'doc', 'docx', 'html'];
......@@ -71,7 +71,7 @@ describe("OpenAssessment.ResponseView", function() {
// Store the args we were passed so we can verify them
this.uploadArgs = {
url: url,
data: data,
data: data
};
// Return a promise indicating success or error
......@@ -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
var baseView = null;
var server = null;
var runtime = {};
var fileUploader = null;
var data = null;
......@@ -120,7 +111,6 @@ describe("OpenAssessment.ResponseView", function() {
server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex');
fileUploader = new StubFileUploader();
baseView = new StubBaseView();
data = {
"ALLOWED_IMAGE_MIME_TYPES": ALLOWED_IMAGE_MIME_TYPES,
"ALLOWED_FILE_MIME_TYPES": ALLOWED_FILE_MIME_TYPES,
......@@ -129,8 +119,9 @@ describe("OpenAssessment.ResponseView", function() {
};
// Create and install the view
var el = $('#openassessment-base').get(0);
view = new OpenAssessment.ResponseView(el, server, fileUploader, baseView, data);
var responseElement = $('#openassessment__response').get(0);
var baseView = new OpenAssessment.BaseView(runtime, responseElement, server, {});
view = new OpenAssessment.ResponseView(responseElement, server, fileUploader, baseView, data);
view.installHandlers();
// Stub the confirmation step
......@@ -296,14 +287,14 @@ describe("OpenAssessment.ResponseView", function() {
}).promise();
});
spyOn(view, 'load');
spyOn(baseView, 'loadAssessmentModules');
spyOn(view.baseView, 'loadAssessmentModules');
view.response(['Test response 1', 'Test response 2']);
view.submit();
// Expect the current and next step to have been reloaded
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() {
......@@ -442,39 +433,45 @@ describe("OpenAssessment.ResponseView", 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: ''}];
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() {
spyOn(baseView, 'toggleActionError').and.callThrough();
spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'image/jpg', size: 1024, name: 'picture.exe', data: ''}];
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() {
spyOn(baseView, 'toggleActionError').and.callThrough();
spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
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() {
spyOn(baseView, 'toggleActionError').and.callThrough();
spyOn(view.baseView, 'toggleActionError').and.callThrough();
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
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() {
spyOn(baseView, 'toggleActionError').and.callThrough();
spyOn(view.baseView, 'toggleActionError').and.callThrough();
view.data.FILE_TYPE_WHITE_LIST = ['exe'];
var files = [{type: 'application/exe', size: 1024, name: 'application.exe', data: ''}];
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() {
......@@ -504,7 +501,7 @@ describe("OpenAssessment.ResponseView", 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
server.uploadUrlError = true;
spyOn(baseView, 'toggleActionError').and.callThrough();
spyOn(view.baseView, 'toggleActionError').and.callThrough();
// Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
......@@ -512,13 +509,13 @@ describe("OpenAssessment.ResponseView", function() {
view.fileUpload();
// 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() {
// Configure the file upload server to return an error
fileUploader.uploadError = true;
spyOn(baseView, 'toggleActionError').and.callThrough();
spyOn(view.baseView, 'toggleActionError').and.callThrough();
// Attempt to upload a file
var files = [{type: 'image/jpeg', size: 1024, name: 'picture.jpg', data: ''}];
......@@ -526,6 +523,6 @@ describe("OpenAssessment.ResponseView", function() {
view.fileUpload();
// 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() {
};
};
// 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
var baseView = null;
var runtime = {};
var server = null;
// View under test
......@@ -43,13 +34,12 @@ describe("OpenAssessment.SelfView", function() {
// Create a new stub server
server = new StubServer();
// Create the stub base view
baseView = new StubBaseView();
server.renderLatex = jasmine.createSpy('renderLatex');
// Create the object under test
var el = $("#openassessment").get(0);
view = new OpenAssessment.SelfView(el, server, baseView);
var assessmentElement = $("#openassessment__self-assessment").get(0);
var baseView = new OpenAssessment.BaseView(runtime, assessmentElement, server, {});
view = new OpenAssessment.SelfView(assessmentElement, server, baseView);
view.installHandlers();
});
......
......@@ -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() {
var server = this;
return $.Deferred(function(defer) {
......@@ -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
var baseView = null;
var server = null;
var runtime = {};
/**
* Create a staff area view.
......@@ -69,8 +67,9 @@ describe('OpenAssessment.StaffAreaView', function() {
if (serverResponse) {
server.data = serverResponse;
}
var el = $('#openassessment').get(0);
var view = new OpenAssessment.StaffAreaView(el, server, baseView);
var assessmentElement = $('#openassessment').get(0);
var baseView = new OpenAssessment.BaseView(runtime, assessmentElement, server, {});
var view = new OpenAssessment.StaffAreaView(assessmentElement, server, baseView);
view.load();
return view;
};
......@@ -93,8 +92,6 @@ describe('OpenAssessment.StaffAreaView', function() {
// Create a new stub server
server = new StubServer();
server.renderLatex = jasmine.createSpy('renderLatex');
// Create the stub base view
baseView = new StubBaseView();
});
describe('Initial rendering', function() {
......@@ -165,46 +162,64 @@ describe('OpenAssessment.StaffAreaView', function() {
});
});
describe('Submission Management', function() {
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();
});
// 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);
describe('Student Info', function() {
var chooseStudent = function(view, studentName) {
var studentNameField = $('.openassessment__student_username', view.element),
submitButton = $('.action--submit-username', view.element);
studentNameField.val(studentName);
submitButton.click();
};
// Response is whitespace --> cancel submission button disabled
view.comment(' \n \n ');
view.handleCommentChanged();
expect(view.cancelSubmissionEnabled()).toBe(false);
beforeEach(function() {
loadFixtures('oa_base_course_staff.html');
appendLoadFixtures('oa_student_info.html');
});
// Response is not blank --> cancel submission button enabled
view.comment('Cancellation reason.');
view.handleCommentChanged();
expect(view.cancelSubmissionEnabled()).toBe(true);
it('shows an error when clicking "Submit" with no student name chosen', function() {
var staffArea = createStaffArea();
chooseStudent(staffArea, '');
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() {
spyOn(server, 'cancelSubmission').and.callThrough();
describe('Submission Management', function() {
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
loadFixtures('oa_student_info.html');
var view = createStaffArea();
it('submits the cancel submission comments to the server', function() {
spyOn(server, 'cancelSubmission').and.callThrough();
view.comment('Cancellation reason.');
view.cancelSubmission('Bob');
var staffArea = createStaffArea();
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() {
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
var baseView = null;
var server = null;
var runtime = {};
// View under test
var view = null;
......@@ -50,12 +41,11 @@ describe("OpenAssessment.StudentTrainingView", function() {
// Create a new stub server
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.StudentTrainingView(el, server, baseView);
var trainingElement = $('#openassessment__student-training').get(0);
var baseView = new OpenAssessment.BaseView(runtime, trainingElement, server, {});
view = new OpenAssessment.StudentTrainingView(trainingElement, server, baseView);
view.installHandlers();
});
......@@ -112,7 +102,7 @@ describe("OpenAssessment.StudentTrainingView", function() {
// Simulate that the user answered the problem correctly, so there are no corrections
server.corrections = {};
spyOn(server, 'trainingAssess').and.callThrough();
spyOn(baseView, 'loadAssessmentModules').and.callThrough();
spyOn(view.baseView, 'loadAssessmentModules').and.callThrough();
// Select rubric options
var optionsSelected = {};
......@@ -128,6 +118,6 @@ describe("OpenAssessment.StudentTrainingView", function() {
expect(server.trainingAssess).toHaveBeenCalledWith(optionsSelected);
// 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.
// Set the fixture path
jasmine.getFixtures().fixturesPath = 'base/fixtures';
......@@ -43,13 +43,12 @@ OpenAssessment.BaseView.prototype = {
},
/**
Install click handlers to expand/collapse a section.
Args:
parentSel (JQuery selector): CSS selector for the container element.
**/
setUpCollapseExpand: function(parentSel) {
parentSel.on('click', '.ui-toggle-visibility__control', function(eventData) {
* Install click handlers to expand/collapse a section.
*
* @param {element} parentElement JQuery selector for the container element.
*/
setUpCollapseExpand: function(parentElement) {
parentElement.on('click', '.ui-toggle-visibility__control', function(eventData) {
var sel = $(eventData.target).closest('.ui-toggle-visibility');
sel.toggleClass('is--collapsed');
}
......@@ -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() {
this.responseView.load();
this.loadAssessmentModules();
......@@ -66,9 +65,9 @@ OpenAssessment.BaseView.prototype = {
},
/**
Refresh the Assessment Modules. This should be called any time an action is
performed by the user.
**/
* Refresh the Assessment Modules. This should be called any time an action is
* performed by the user.
*/
loadAssessmentModules: function() {
this.trainingView.load();
this.peerView.load();
......@@ -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() {
this.messageView.load();
},
/**
Report an error to the user.
Args:
type (str): Which type of error. Options are "save", submit", "peer", and "self".
msg (str or null): The error message to display.
If null, hide the error message (with one exception: loading errors are never hidden once displayed)
**/
toggleActionError: function(type, msg) {
* Report an error to the user.
*
* @param {string} type The type of error. Options are "save", submit", "peer", and "self".
* @param {string} message The error message to display, or if null hide the message.
* Note: loading errors are never hidden once displayed.
*/
toggleActionError: function(type, message) {
var element = this.element;
var container = null;
if (type === 'save') {
......@@ -123,29 +121,32 @@ OpenAssessment.BaseView.prototype = {
// If we don't have anywhere to put the message, just log it to the console
if (container === null) {
if (msg !== null) { console.log(msg); }
if (message !== null) { console.log(message); }
}
else {
// Insert the error message
var msgHtml = (msg === null) ? "" : msg;
var msgHtml = (message === null) ? "" : message;
$(container + " .message__content", element).html('<p>' + msgHtml + '</p>');
// Toggle the error class
$(container, element).toggleClass('has--error', msg !== null);
$(container, element).toggleClass('has--error', message !== null);
}
},
/**
Report an error loading a step.
Args:
step (str): the step that could not be loaded.
**/
showLoadError: function(step) {
var container = '#openassessment__' + step;
$(container).toggleClass('has--error', true);
$(container + ' .step__status__value i').removeClass().addClass('icon fa fa-exclamation-triangle');
$(container + ' .step__status__value .copy').html(gettext('Unable to Load'));
* Report an error loading a step.
*
* @param {string} stepName The step that could not be loaded.
* @param {string} errorMessage An optional error message to use instead of the default.
*/
showLoadError: function(stepName, errorMessage) {
if (!errorMessage) {
errorMessage = 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 @@
.ui-staff__content {
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 @@
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 {
@extend %t-strong;
}
......@@ -54,10 +79,7 @@
}
.ui-staff__content__section {
padding-bottom: $baseline-v;
border-bottom: 1px solid rgba($color-decorative-staff, 0.25);
margin-bottom: $baseline-v;
padding-bottom: ($baseline-v/2);
@extend %wipe-last-child;
}
......@@ -110,7 +132,7 @@
}
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);
}
......@@ -136,7 +158,7 @@
}
// UI - cancel submission (action)
.openassessment__staff-info__cancel__submission {
.staff-info__workflow-cancellation {
.staff-info__cancel-submission__content {
......
......@@ -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));
}
}
.staff-info__student {
.label {
color: $heading-staff-color;
margin-bottom: ($baseline-v/2);
input {
display: block;
}
}
.action--submit {
......@@ -129,33 +134,115 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
}
.title--sub {
@extend %hd-4;
color: $heading-staff-color;
margin-top: ($baseline-v/2);
margin-bottom: ($baseline-v/2);
}
.student__answer__display__content {
border: 1px solid rgba($color-decorative-staff, 0.25);
@include padding(($baseline-v/2), ($baseline-h/2), ($baseline-v/2), ($baseline-h/2));
border: 1px solid rgba($copy-staff-color, 0.25);
padding: ($baseline-v/2) ($baseline-h/4);
margin-bottom: ($baseline-v/2);
}
.openassessment__student-info_list {
.staff-info__student__report {
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 {
width: $max-width/2;
}
/**
* The follow styles are bound for the "shame" file. This is done to override
* LMS specific styles on HTML elements.
*/
// 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;
}
// 'p' elements in LMS have a color set on them.
.student__answer__display__content p {
color: inherit;
// 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);
}
}
}
}
......@@ -1144,6 +1231,8 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
.self-assessment__display__title,
.peer-assessment__display__header
.peer-assessment__display__title,
.staff-assessment__display__header
.staff-assessment__display__title,
.submission__answer__display
.submission__answer__display__title{
margin: 10px 0;
......@@ -1152,6 +1241,7 @@ $link-hover: $edx-blue-l1 !default; // from our Pattern Library http://ux.edx.or
.self-assessment__display__image,
.peer-assessment__display__image,
.staff-assessment__display__image,
.submission__answer__display__image{
@extend .submission__answer__display__content;
max-height: 400px;
......
......@@ -193,7 +193,14 @@
color: $copy-staff-color !important;
}
.openassessment__staff-info__cancel__submission {
.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;
}
}
......@@ -110,21 +110,27 @@ class WorkflowMixin(object):
requirements = self.workflow_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.
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:
dict
Raises:
AssessmentWorkflowError
"""
if not self.submission_uuid:
return {}
if not submission_uuid:
submission_uuid = self.submission_uuid
if not submission_uuid:
return {}
return workflow_api.get_workflow_for_submission(
self.submission_uuid, self.workflow_requirements()
submission_uuid, self.workflow_requirements()
)
def get_workflow_status_counts(self):
......
......@@ -25,7 +25,6 @@ class OpenAssessmentA11yTest(OpenAssessmentTest):
)
page.a11y_audit.config.set_rules({
"ignore": [
"aria-valid-attr", # TODO: AC-199
"color-contrast", # TODO: AC-198
"empty-heading", # TODO: AC-197
"link-href", # TODO: AC-199
......@@ -77,12 +76,14 @@ class StudentTrainingA11yTest(OpenAssessmentA11yTest):
class StaffAreaA11yTest(OpenAssessmentA11yTest):
"""
Test the accessibility of the staff area.
This is testing a problem with "self assessment only".
"""
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)
def test_staff_tools_panel_a11y(self):
def test_staff_tools_panel(self):
"""
Check the accessibility of the "Staff Tools" panel
"""
......@@ -90,7 +91,7 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest):
self.staff_area_page.click_staff_toolbar_button("staff-tools")
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
"""
......@@ -98,6 +99,20 @@ class StaffAreaA11yTest(OpenAssessmentA11yTest):
self.staff_area_page.click_staff_toolbar_button("staff-info")
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__":
......
......@@ -79,3 +79,14 @@ class AutoAuthPage(PageObject):
message = self.q(css='BODY').text[0].strip()
match = re.search(r' user_id ([^$]+)$', message)
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):
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):
return self.q(css="#openassessment__staff-area").is_present()
return self.q(css=".openassessment__staff-area").is_present()
@property
def selected_button_names(self):
......@@ -360,10 +360,10 @@ class StaffAreaPage(OpenAssessmentPage):
@property
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")
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):
"""
......@@ -379,3 +379,29 @@ class StaffAreaPage(OpenAssessmentPage):
:return:
"""
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):
self.student_training_page = AssessmentPage('student-training', 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):
"""
......@@ -103,18 +124,7 @@ class SelfAssessmentTest(OpenAssessmentTest):
@attr('acceptance')
def test_self_assessment(self):
# Submit a response
self.auto_auth_page.visit()
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)
self.do_self_assessment()
# Check browser scrolled back to top of assessment
self.assertTrue(self.self_asmnt_page.is_on_top)
......@@ -216,10 +226,12 @@ class StudentTrainingTest(OpenAssessmentTest):
class StaffAreaTest(OpenAssessmentTest):
"""
Test the staff area.
This is testing a problem with "self assessment only".
"""
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)
@retry()
......@@ -275,9 +287,9 @@ class StaffAreaTest(OpenAssessmentTest):
# Click on the button and verify that the panel has opened
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.visible_staff_panels,
[u'openassessment__{button_name}'.format(button_name=panel_name)]
self.assertIn(
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
......@@ -285,6 +297,51 @@ class StaffAreaTest(OpenAssessmentTest):
self.assertEqual(self.staff_area_page.selected_button_names, [])
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):
"""
......
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