Commit b93fd0d7 by Stephen Sanchez

Merge pull request #351 from edx/sanchez/TIM-583-schedule-training

TIM 583 schedule training button admin only
parents 6883bfc0 476285c7
......@@ -78,6 +78,13 @@
</table>
</div>
{% if display_schedule_training %}
<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>
</div>
{% endif %}
<div class="staff-info__student ui-staff__content__section">
<div class="wrapper--input" class="staff-info__student__form">
<form id="openassessment_student_info_form">
......
......@@ -68,6 +68,7 @@ UI_MODELS = {
VALID_ASSESSMENT_TYPES = [
"student-training",
"example-based-assessment",
"peer-assessment",
"self-assessment",
]
......@@ -231,6 +232,19 @@ class OpenAssessmentBlock(
return frag
@property
def is_admin(self):
"""
Check whether the user has global staff permissions.
Returns:
bool
"""
if hasattr(self, 'xmodule_runtime'):
return getattr(self.xmodule_runtime, 'user_is_admin', False)
else:
return False
@property
def is_course_staff(self):
"""
Check whether the user has course staff permissions for this XBlock.
......@@ -243,6 +257,8 @@ class OpenAssessmentBlock(
else:
return False
@property
def in_studio_preview(self):
"""
......
......@@ -6,10 +6,12 @@ import copy
from django.utils.translation import ugettext as _
from xblock.core import XBlock
from openassessment.assessment.errors.ai import AIError
from openassessment.xblock.resolve_dates import DISTANT_PAST, DISTANT_FUTURE
from submissions import api as submission_api
from openassessment.assessment.api import peer as peer_api
from openassessment.assessment.api import self as self_api
from openassessment.assessment.api import ai as ai_api
class StaffInfoMixin(object):
......@@ -29,8 +31,14 @@ class StaffInfoMixin(object):
return self.render_error(_(
u"You do not have permission to access staff information"
))
student_item = self.get_student_item_dict()
context = dict()
path, context = self.get_staff_path_and_context()
return self.render_assessment(path, context)
def get_staff_path_and_context(self):
"""
Gets the path and context for the staff section of the ORA XBlock.
"""
context = {}
path = 'openassessmentblock/staff_debug/staff_debug.html'
# Calculate how many students are in each step of the workflow
......@@ -38,6 +46,11 @@ class StaffInfoMixin(object):
context['status_counts'] = status_counts
context['num_submissions'] = num_submissions
# Show the schedule training button if example based assessment is
# configured, and the current user has admin privileges.
assessment = self.get_assessment_module('example-based-assessment')
context['display_schedule_training'] = self.is_admin and assessment
# We need to display the new-style locations in the course staff
# info, even if we're using old-style locations internally,
# so course staff can use the locations to delete student state.
......@@ -57,8 +70,41 @@ class StaffInfoMixin(object):
'start': start_date if start_date > DISTANT_PAST else None,
'due': due_date if due_date < DISTANT_FUTURE else None,
})
return path, context
return self.render_assessment(path, context)
@XBlock.json_handler
def schedule_training(self, data, suffix=''):
if not self.is_admin or self.in_studio_preview:
return {
'success': False,
'msg': _(u"You do not have permission to schedule training")
}
assessment = self.get_assessment_module('example-based-assessment')
if assessment:
examples = assessment["examples"]
try:
workflow_uuid = ai_api.train_classifiers(
self.rubric_criteria,
examples,
assessment["algorithm_id"]
)
return {
'success': True,
'workflow_uuid': workflow_uuid,
'msg': _(u"Training scheduled with new Workflow UUID: {}".format(workflow_uuid))
}
except AIError as err:
return {
'success': False,
'msg': _(u"An error occurred scheduling classifier training {}".format(err))
}
else:
return {
'success': False,
'msg': _(u"Example Based Assessment is not configured for this location.")
}
@XBlock.handler
def render_student_info(self, data, suffix=''):
......@@ -77,7 +123,7 @@ class StaffInfoMixin(object):
if not self.is_course_staff or self.in_studio_preview:
return self.render_error(_(
u"You do not have permission to access student information."
))
))
path, context = self.get_student_info_path_and_context(data)
return self.render_assessment(path, context)
......
......@@ -345,5 +345,20 @@
"template": "openassessmentblock/oa_edit.html",
"context": {},
"output": "oa_edit.html"
},
{
"template": "openassessmentblock/staff_debug/staff_debug.html",
"output": "staff_debug.html",
"context": {
"status_counts": [
{
"status": "peer",
"count": 0
}
],
"item_id": ".openassessment.d91.u0",
"display_schedule_training": true,
"num_submissions": 0
}
}
]
/**
Tests for OpenAssessment Student Training view.
**/
describe("OpenAssessment.StaffInfoView", function() {
// Stub server
var StubServer = function() {
var successPromise = $.Deferred(
function(defer) { defer.resolve(); }
).promise();
this.render = function(step) {
return successPromise;
};
this.scheduleTraining = function() {
var server = this;
return $.Deferred(function(defer) {
defer.resolveWith(server, [server.data]);
}).promise();
};
this.data = {};
};
// 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;
// View under test
var view = null;
beforeEach(function() {
// Load the DOM fixture
jasmine.getFixtures().fixturesPath = 'base/fixtures';
loadFixtures('staff_debug.html');
// 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.StaffInfoView(el, server, baseView);
view.installHandlers();
});
it("schedules training of AI classifiers", function() {
server.data = {
"success": true,
"workflow_uuid": "abc123",
"msg": "Great success."
};
spyOn(server, 'scheduleTraining').andCallThrough();
// Submit the assessment
view.scheduleTraining();
// Expect that the assessment was sent to the server
expect(server.scheduleTraining).toHaveBeenCalled();
});
});
......@@ -337,6 +337,40 @@ OpenAssessment.Server.prototype = {
},
/**
Schedules classifier training for Example Based Assessment for this
Location.
Returns:
A JQuery promise, which resolves with a message indicating the results
of the scheduling request.
Example:
server.scheduleTraining().done(
function(msg) { console.log("Success!"); }
alert(msg);
).fail(
function(errorMsg) { console.log(errorMsg); }
);
**/
scheduleTraining: function() {
var url = this.url('schedule_training');
return $.Deferred(function(defer) {
$.ajax({ type: "POST", url: url, data: "\"\""}).done(
function(data) {
if (data.success) {
defer.resolveWith(this, [data.msg]);
}
else {
defer.rejectWith(this, [data.msg]);
}
}
).fail(function(data) {
defer.rejectWith(this, [gettext('This assessment could not be submitted.')]);
});
});
},
/**
Load the XBlock's XML definition from the server.
Returns:
......
......@@ -81,5 +81,29 @@ OpenAssessment.StaffInfoView.prototype = {
view.loadStudentInfo();
}
);
// Install a click handler for scheduling AI classifier training
sel.find('#schedule_training').click(
function(eventObject) {
eventObject.preventDefault();
view.scheduleTraining();
}
);
},
/**
Sends a request to the server to schedule the training of classifiers for
this problem's Example Based Assessments.
**/
scheduleTraining: function() {
var view = this;
this.server.scheduleTraining().done(
function(msg) {
$('#schedule_training_message', this.element).text(msg)
}
).fail(function(errMsg) {
$('#schedule_training_message', this.element).text(errMsg)
});
}
};
......@@ -26,6 +26,13 @@
// --------------------
// Developer styles for Staff Section
// --------------------
.staff-info__status {
.action--submit {
@extend %btn--secondary;
@extend %action-2;
margin: ($baseline-v/2) ($baseline-v/2) ($baseline-v/2) ($baseline-v/2);
}
}
.staff-info__student {
.label {
color: $heading-staff-color;
......
......@@ -5,7 +5,9 @@ import json
from mock import Mock, patch
from openassessment.assessment.api import peer as peer_api
from openassessment.assessment.api import self as self_api
from openassessment.assessment.api import ai as ai_api
from openassessment.workflow import api as workflow_api
from openassessment.assessment.errors.ai import AIError
from submissions import api as sub_api
from openassessment.xblock.test.base import scenario, XBlockHandlerTestCase
......@@ -25,6 +27,38 @@ ASSESSMENT_DICT = {
},
}
EXAMPLE_BASED_ASSESSMENT = {
"name": "example-based-assessment",
"algorithm_id": "1",
"examples": [
{
"answer": "Foo",
"options_selected": [
{
"criterion": "Ideas",
"option": "Fair"
},
{
"criterion": "Content",
"option": "Good"
}
]
},
{
"answer": "Bar",
"options_selected": [
{
"criterion": "Ideas",
"option": "Poor"
},
{
"criterion": "Content",
"option": "Good"
}
]
}
]
}
class TestCourseStaff(XBlockHandlerTestCase):
"""
......@@ -50,7 +84,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
def test_course_staff_debug_info(self, xblock):
# If we're not course staff, we shouldn't see the debug info
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, False, "Bob"
xblock.scope_ids.usage_id, False, False, "Bob"
)
resp = self.request(xblock, 'render_staff_info', json.dumps({}))
self.assertNotIn("course staff information", resp.decode('utf-8').lower())
......@@ -64,7 +98,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
def test_course_student_debug_info(self, xblock):
# If we're not course staff, we shouldn't see the debug info
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, False, "Bob"
xblock.scope_ids.usage_id, False, False, "Bob"
)
resp = self.request(xblock, 'render_student_info', json.dumps({}))
self.assertIn("you do not have permission", resp.decode('utf-8').lower())
......@@ -81,7 +115,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
# In this case, the runtime will tell us that we're staff,
# but no user ID will be set.
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, "Bob"
xblock.scope_ids.usage_id, True, False, "Bob"
)
resp = self.request(xblock, 'render_staff_info', json.dumps({}))
self.assertNotIn("course staff information", resp.decode('utf-8').lower())
......@@ -90,7 +124,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
def test_staff_debug_dates_table(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, "Bob"
xblock.scope_ids.usage_id, True, False, "Bob"
)
# Verify that we can render without error
......@@ -111,7 +145,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
def test_staff_debug_dates_distant_past_and_future(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, "Bob"
xblock.scope_ids.usage_id, True, False, "Bob"
)
# Verify that we can render without error
......@@ -123,7 +157,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
def test_staff_debug_student_info_no_submission(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, "Bob"
xblock.scope_ids.usage_id, True, False, "Bob"
)
request = namedtuple('Request', 'params')
request.params = {"student_id": "test_student"}
......@@ -135,7 +169,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
def test_staff_debug_student_info_peer_only(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, "Bob"
xblock.scope_ids.usage_id, True, False, "Bob"
)
bob_item = STUDENT_ITEM.copy()
......@@ -175,7 +209,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
def test_staff_debug_student_info_self_only(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, "Bob"
xblock.scope_ids.usage_id, True, False, "Bob"
)
bob_item = STUDENT_ITEM.copy()
......@@ -206,7 +240,7 @@ class TestCourseStaff(XBlockHandlerTestCase):
def test_staff_debug_student_info_full_workflow(self, xblock):
# Simulate that we are course staff
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, "Bob"
xblock.scope_ids.usage_id, True, False, "Bob"
)
bob_item = STUDENT_ITEM.copy()
......@@ -248,14 +282,75 @@ class TestCourseStaff(XBlockHandlerTestCase):
resp = xblock.render_student_info(request)
self.assertIn("bob answer", resp.body.lower())
def _create_mock_runtime(self, item_id, is_staff, anonymous_user_id):
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_display_schedule_training(self, xblock):
xblock.rubric_assessments.append(EXAMPLE_BASED_ASSESSMENT)
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, True, "Bob"
)
path, context = xblock.get_staff_path_and_context()
self.assertEquals('openassessmentblock/staff_debug/staff_debug.html', path)
self.assertTrue(context['display_schedule_training'])
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_schedule_training(self, xblock):
xblock.rubric_assessments.append(EXAMPLE_BASED_ASSESSMENT)
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, True, "Bob"
)
response = self.request(xblock, 'schedule_training', json.dumps({}), response_format='json')
self.assertTrue(response['success'])
self.assertTrue('workflow_uuid' in response)
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_not_displaying_schedule_training(self, xblock):
xblock.rubric_assessments.append(EXAMPLE_BASED_ASSESSMENT)
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, False, "Bob"
)
path, context = xblock.get_staff_path_and_context()
self.assertEquals('openassessmentblock/staff_debug/staff_debug.html', path)
self.assertFalse(context['display_schedule_training'])
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_admin_schedule_training_no_permissions(self, xblock):
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, False, "Bob"
)
response = self.request(xblock, 'schedule_training', json.dumps({}), response_format='json')
self.assertFalse(response['success'])
self.assertTrue('permission' in response['msg'])
@patch.object(ai_api, "train_classifiers")
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_admin_schedule_training_error(self, xblock, mock_api):
mock_api.side_effect = AIError("Oh no!")
xblock.rubric_assessments.append(EXAMPLE_BASED_ASSESSMENT)
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, True, "Bob"
)
response = self.request(xblock, 'schedule_training', json.dumps({}), response_format='json')
self.assertFalse(response['success'])
self.assertTrue('error' in response['msg'])
@scenario('data/basic_scenario.xml', user_id='Bob')
def test_no_example_based_assessment(self, xblock):
xblock.xmodule_runtime = self._create_mock_runtime(
xblock.scope_ids.usage_id, True, True, "Bob"
)
response = self.request(xblock, 'schedule_training', json.dumps({}), response_format='json')
self.assertFalse(response['success'])
self.assertTrue('not configured' in response['msg'])
def _create_mock_runtime(self, item_id, is_staff, is_admin, anonymous_user_id):
mock_runtime = Mock(
course_id='test_course',
item_id=item_id,
anonymous_student_id='Bob',
user_is_staff=is_staff,
user_is_admin=is_admin,
service=lambda self, service: Mock(
get_anonymous_student_id=lambda user_id, course_id: anonymous_user_id
)
)
return mock_runtime
\ No newline at end of file
return mock_runtime
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