Merge pull request #2686 from edx/feature/ichuang/add-reset-links-to-staff-debug
add problem reset link to staff debug page
This commit is contained in:
84
common/test/acceptance/pages/lms/staff_view.py
Normal file
84
common/test/acceptance/pages/lms/staff_view.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Staff view of courseware
|
||||
"""
|
||||
from bok_choy.page_object import PageObject
|
||||
|
||||
|
||||
class StaffPage(PageObject):
|
||||
"""
|
||||
View of courseware pages while logged in as course staff
|
||||
"""
|
||||
|
||||
url = None
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='#staffstatus').present
|
||||
|
||||
@property
|
||||
def staff_status(self):
|
||||
"""
|
||||
Return the current status, either Staff view or Student view
|
||||
"""
|
||||
return self.q(css='#staffstatus').text[0]
|
||||
|
||||
def open_staff_debug_info(self):
|
||||
"""
|
||||
Open the staff debug window
|
||||
Return the page object for it.
|
||||
"""
|
||||
self.q(css='a.instructor-info-action').first.click()
|
||||
staff_debug_page = StaffDebugPage(self.browser)
|
||||
staff_debug_page.wait_for_page()
|
||||
return staff_debug_page
|
||||
|
||||
def answer_problem(self):
|
||||
"""
|
||||
Answers the problem to give state that we can clean
|
||||
"""
|
||||
self.q(css='input.check').first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
|
||||
class StaffDebugPage(PageObject):
|
||||
"""
|
||||
Staff Debug modal
|
||||
"""
|
||||
|
||||
url = None
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='section.staff-modal').present
|
||||
|
||||
def reset_attempts(self, user=None):
|
||||
"""
|
||||
This clicks on the reset attempts link with an optionally
|
||||
specified user.
|
||||
"""
|
||||
if user:
|
||||
self.q(css='input[id^=sd_fu_]').first.fill(user)
|
||||
self.q(css='section.staff-modal a#staff-debug-reset').click()
|
||||
|
||||
def delete_state(self, user=None):
|
||||
"""
|
||||
This delete's a student's state for the problem
|
||||
"""
|
||||
if user:
|
||||
self.q(css='input[id^=sd_fu_]').fill(user)
|
||||
self.q(css='section.staff-modal a#staff-debug-sdelete').click()
|
||||
|
||||
def rescore(self, user=None):
|
||||
"""
|
||||
This clicks on the reset attempts link with an optionally
|
||||
specified user.
|
||||
"""
|
||||
if user:
|
||||
self.q(css='input[id^=sd_fu_]').first.fill(user)
|
||||
self.q(css='section.staff-modal a#staff-debug-rescore').click()
|
||||
|
||||
@property
|
||||
def idash_msg(self):
|
||||
"""
|
||||
Returns the value of #idash_msg
|
||||
"""
|
||||
self.wait_for_ajax()
|
||||
return self.q(css='#idash_msg').text
|
||||
151
common/test/acceptance/tests/test_staff_view.py
Normal file
151
common/test/acceptance/tests/test_staff_view.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
E2E tests for the LMS.
|
||||
"""
|
||||
|
||||
from .helpers import UniqueCourseTest
|
||||
from ..pages.studio.auto_auth import AutoAuthPage
|
||||
from ..pages.lms.courseware import CoursewarePage
|
||||
from ..pages.lms.staff_view import StaffPage
|
||||
from ..fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from textwrap import dedent
|
||||
|
||||
|
||||
class StaffDebugTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that verify the staff debug info.
|
||||
"""
|
||||
USERNAME = "STAFF_TESTER"
|
||||
EMAIL = "johndoe@example.com"
|
||||
|
||||
def setUp(self):
|
||||
super(StaffDebugTest, self).setUp()
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
|
||||
# Install a course with sections/problems, tabs, updates, and handouts
|
||||
course_fix = CourseFixture(
|
||||
self.course_info['org'], self.course_info['number'],
|
||||
self.course_info['run'], self.course_info['display_name']
|
||||
)
|
||||
|
||||
problem_data = dedent("""
|
||||
<problem markdown="Simple Problem" max_attempts="" weight="">
|
||||
<p>Choose Yes.</p>
|
||||
<choiceresponse>
|
||||
<checkboxgroup direction="vertical">
|
||||
<choice correct="true">Yes</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
course_fix.add_children(
|
||||
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
||||
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
||||
XBlockFixtureDesc('problem', 'Test Problem 1', data=problem_data)
|
||||
)
|
||||
)
|
||||
).install()
|
||||
|
||||
# Auto-auth register for the course.
|
||||
# Do this as global staff so that you will see the Staff View
|
||||
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
|
||||
course_id=self.course_id, staff=True).visit()
|
||||
|
||||
def _goto_staff_page(self):
|
||||
"""
|
||||
Open staff page with assertion
|
||||
"""
|
||||
self.courseware_page.visit()
|
||||
staff_page = StaffPage(self.browser)
|
||||
self.assertEqual(staff_page.staff_status, 'Staff view')
|
||||
return staff_page
|
||||
|
||||
def test_reset_attempts_empty(self):
|
||||
"""
|
||||
Test that we reset even when there is no student state
|
||||
"""
|
||||
|
||||
staff_debug_page = self._goto_staff_page().open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_delete_state_empty(self):
|
||||
"""
|
||||
Test that we delete properly even when there isn't state to delete.
|
||||
"""
|
||||
staff_debug_page = self._goto_staff_page().open_staff_debug_info()
|
||||
staff_debug_page.delete_state()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_reset_attempts_state(self):
|
||||
"""
|
||||
Successfully reset the student attempts
|
||||
"""
|
||||
staff_page = self._goto_staff_page()
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_rescore_state(self):
|
||||
"""
|
||||
Rescore the student
|
||||
"""
|
||||
staff_page = self._goto_staff_page()
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.rescore()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
# Since we aren't running celery stuff, this will fail badly
|
||||
# for now, but is worth excercising that bad of a response
|
||||
self.assertEqual(u'Failed to rescore problem. '
|
||||
'Unknown Error Occurred.', msg)
|
||||
|
||||
def test_student_state_delete(self):
|
||||
"""
|
||||
Successfully delete the student state with an answer
|
||||
"""
|
||||
staff_page = self._goto_staff_page()
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.delete_state()
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully deleted student state '
|
||||
'for user {}'.format(self.USERNAME), msg)
|
||||
|
||||
def test_student_by_email(self):
|
||||
"""
|
||||
Successfully reset the student attempts using their email address
|
||||
"""
|
||||
staff_page = self._goto_staff_page()
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.reset_attempts(self.EMAIL)
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Successfully reset the attempts '
|
||||
'for user {}'.format(self.EMAIL), msg)
|
||||
|
||||
def test_bad_student(self):
|
||||
"""
|
||||
Test negative response with invalid user
|
||||
"""
|
||||
staff_page = self._goto_staff_page()
|
||||
staff_page.answer_problem()
|
||||
|
||||
staff_debug_page = staff_page.open_staff_debug_info()
|
||||
staff_debug_page.delete_state('INVALIDUSER')
|
||||
msg = staff_debug_page.idash_msg[0]
|
||||
self.assertEqual(u'Failed to delete student state. '
|
||||
'User does not exist.', msg)
|
||||
89
lms/static/js/spec/staff_debug_actions_spec.js
Normal file
89
lms/static/js/spec/staff_debug_actions_spec.js
Normal file
@@ -0,0 +1,89 @@
|
||||
describe('StaffDebugActions', function() {
|
||||
var loc = 'test_loc';
|
||||
var fixture_id = 'sd_fu_' + loc;
|
||||
var fixture = $('<input>', { id: fixture_id, placeholder: "userman" });
|
||||
|
||||
describe('get_url ', function() {
|
||||
it('defines url to courseware ajax entry point', function() {
|
||||
spyOn(StaffDebug, "get_current_url").andReturn("/courses/edX/Open_DemoX/edx_demo_course/courseware/stuff");
|
||||
expect(StaffDebug.get_url('rescore_problem')).toBe('/courses/edX/Open_DemoX/edx_demo_course/instructor_dashboard/api/rescore_problem');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get_user', function() {
|
||||
|
||||
it('gets the placeholder username if input field is empty', function() {
|
||||
$('body').append(fixture);
|
||||
expect(StaffDebug.get_user(loc)).toBe('userman');
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
it('gets a filled in name if there is one', function() {
|
||||
$('body').append(fixture);
|
||||
$('#' + fixture_id).val('notuserman');
|
||||
expect(StaffDebug.get_user(loc)).toBe('notuserman');
|
||||
|
||||
$('#' + fixture_id).val('');
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
describe('reset', function() {
|
||||
it('makes an ajax call with the expected parameters', function() {
|
||||
$('body').append(fixture);
|
||||
|
||||
spyOn($, 'ajax');
|
||||
StaffDebug.reset(loc);
|
||||
|
||||
expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET');
|
||||
expect($.ajax.mostRecentCall.args[0]['data']).toEqual({
|
||||
'problem_to_reset': loc,
|
||||
'unique_student_identifier': 'userman',
|
||||
'delete_module': false
|
||||
});
|
||||
expect($.ajax.mostRecentCall.args[0]['url']).toEqual(
|
||||
'/instructor_dashboard/api/reset_student_attempts'
|
||||
);
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
describe('sdelete', function() {
|
||||
it('makes an ajax call with the expected parameters', function() {
|
||||
$('body').append(fixture);
|
||||
|
||||
spyOn($, 'ajax');
|
||||
StaffDebug.sdelete(loc);
|
||||
|
||||
expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET');
|
||||
expect($.ajax.mostRecentCall.args[0]['data']).toEqual({
|
||||
'problem_to_reset': loc,
|
||||
'unique_student_identifier': 'userman',
|
||||
'delete_module': true
|
||||
});
|
||||
expect($.ajax.mostRecentCall.args[0]['url']).toEqual(
|
||||
'/instructor_dashboard/api/reset_student_attempts'
|
||||
);
|
||||
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
describe('rescore', function() {
|
||||
it('makes an ajax call with the expected parameters', function() {
|
||||
$('body').append(fixture);
|
||||
|
||||
spyOn($, 'ajax');
|
||||
StaffDebug.rescore(loc);
|
||||
|
||||
expect($.ajax.mostRecentCall.args[0]['type']).toEqual('GET');
|
||||
expect($.ajax.mostRecentCall.args[0]['data']).toEqual({
|
||||
'problem_to_reset': loc,
|
||||
'unique_student_identifier': 'userman',
|
||||
'delete_module': false
|
||||
});
|
||||
expect($.ajax.mostRecentCall.args[0]['url']).toEqual(
|
||||
'/instructor_dashboard/api/rescore_problem'
|
||||
);
|
||||
$('#' + fixture_id).remove();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
126
lms/static/js/staff_debug_actions.js
Normal file
126
lms/static/js/staff_debug_actions.js
Normal file
@@ -0,0 +1,126 @@
|
||||
// Build StaffDebug object
|
||||
var StaffDebug = (function(){
|
||||
|
||||
get_current_url = function() {
|
||||
return window.location.pathname;
|
||||
}
|
||||
|
||||
get_url = function(action){
|
||||
var pathname = this.get_current_url();
|
||||
var url = pathname.substr(0,pathname.indexOf('/courseware')) + '/instructor_dashboard/api/' + action;
|
||||
return url;
|
||||
}
|
||||
|
||||
get_user = function(locname){
|
||||
var uname = $('#sd_fu_' + locname).val();
|
||||
if (uname==""){
|
||||
uname = $('#sd_fu_' + locname).attr('placeholder');
|
||||
}
|
||||
return uname;
|
||||
}
|
||||
|
||||
do_idash_action = function(action){
|
||||
var pdata = {
|
||||
'problem_to_reset': action.location,
|
||||
'unique_student_identifier': get_user(action.location),
|
||||
'delete_module': action.delete_module
|
||||
}
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: get_url(action.method),
|
||||
data: pdata,
|
||||
success: function(data){
|
||||
var text = _.template(
|
||||
action.success_msg,
|
||||
{user: data.student},
|
||||
{interpolate: /\{(.+?)\}/g}
|
||||
)
|
||||
var html = _.template(
|
||||
'<p id="idash_msg" class="success">{text}</p>',
|
||||
{text: text},
|
||||
{interpolate: /\{(.+?)\}/g}
|
||||
)
|
||||
$("#result_"+action.location).html(html);
|
||||
},
|
||||
error: function(request, status, error) {
|
||||
var response_json;
|
||||
try {
|
||||
response_json = $.parseJSON(request.responseText);
|
||||
} catch(e) {
|
||||
response_json = { error: gettext('Unknown Error Occurred.') };
|
||||
}
|
||||
var text = _.template(
|
||||
'{error_msg} {error}',
|
||||
{
|
||||
error_msg: action.error_msg,
|
||||
error: response_json.error
|
||||
},
|
||||
{interpolate: /\{(.+?)\}/g}
|
||||
)
|
||||
var html = _.template(
|
||||
'<p id="idash_msg" class="error">{text}</p>',
|
||||
{text: text},
|
||||
{interpolate: /\{(.+?)\}/g}
|
||||
)
|
||||
$("#result_"+action.location).html(html);
|
||||
},
|
||||
dataType: 'json'
|
||||
});
|
||||
}
|
||||
|
||||
reset = function(locname){
|
||||
this.do_idash_action({
|
||||
location: locname,
|
||||
method: 'reset_student_attempts',
|
||||
success_msg: gettext('Successfully reset the attempts for user {user}'),
|
||||
error_msg: gettext('Failed to reset attempts.'),
|
||||
delete_module: false
|
||||
});
|
||||
}
|
||||
|
||||
sdelete = function(locname){
|
||||
this.do_idash_action({
|
||||
location: locname,
|
||||
method: 'reset_student_attempts',
|
||||
success_msg: gettext('Successfully deleted student state for user {user}'),
|
||||
error_msg: gettext('Failed to delete student state.'),
|
||||
delete_module: true
|
||||
});
|
||||
}
|
||||
|
||||
rescore = function(locname){
|
||||
this.do_idash_action({
|
||||
location: locname,
|
||||
method: 'rescore_problem',
|
||||
success_msg: gettext('Successfully rescored problem for user {user}'),
|
||||
error_msg: gettext('Failed to rescore problem.'),
|
||||
delete_module: false
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
reset: reset,
|
||||
sdelete: sdelete,
|
||||
rescore: rescore,
|
||||
do_idash_action: do_idash_action,
|
||||
get_current_url: get_current_url,
|
||||
get_url: get_url,
|
||||
get_user: get_user
|
||||
}
|
||||
})();
|
||||
|
||||
// Register click handlers
|
||||
$(document).ready(function() {
|
||||
$('#staff-debug-reset').click(function() {
|
||||
StaffDebug.reset($(this).data('location'));
|
||||
return false;
|
||||
});
|
||||
$('#staff-debug-sdelete').click(function() {
|
||||
StaffDebug.sdelete($(this).data('location'));
|
||||
return false;
|
||||
});
|
||||
$('#staff-debug-rescore').click(function() {
|
||||
StaffDebug.rescore($(this).data('location'));
|
||||
return false;
|
||||
});
|
||||
});
|
||||
@@ -51,6 +51,7 @@ lib_paths:
|
||||
src_paths:
|
||||
- coffee/src
|
||||
- js/src
|
||||
- js
|
||||
|
||||
# Paths to spec (test) JavaScript files
|
||||
spec_paths:
|
||||
|
||||
@@ -191,6 +191,15 @@ div.course-wrapper {
|
||||
}
|
||||
}
|
||||
|
||||
div.staff_actions {
|
||||
p.error {
|
||||
color: $error-red
|
||||
}
|
||||
p.success {
|
||||
color: $success-color;
|
||||
}
|
||||
}
|
||||
|
||||
div.staff_info {
|
||||
display: none;
|
||||
@include clearfix();
|
||||
|
||||
@@ -592,7 +592,7 @@ function goto( mode)
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
%if msg:
|
||||
<p></p><p>${msg}</p>
|
||||
<p></p><p id="idash_msg">${msg}</p>
|
||||
%endif
|
||||
|
||||
##-----------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/vendor/jquery.leanModal.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/staff_debug_actions.js')}"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function setup_debug(element_id, edit_link, staff_context){
|
||||
@@ -91,4 +92,4 @@ function getlog(element_id, staff_context){
|
||||
|
||||
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
## The JS for this is defined in xqa_interface.html
|
||||
${block_content}
|
||||
@@ -52,6 +53,27 @@ ${block_content}
|
||||
<header>
|
||||
<h2>${_('Staff Debug')}</h2>
|
||||
</header>
|
||||
|
||||
<hr />
|
||||
<div class="staff_actions">
|
||||
<h3>${_('Actions')}</h3>
|
||||
<div>
|
||||
<label for="sd_fu_${location.name}">${_('Username')}:</label>
|
||||
<input type="text" id="sd_fu_${location.name}" placeholder="${user.username}"/>
|
||||
</div>
|
||||
<div>
|
||||
[
|
||||
<a href="#" id="staff-debug-reset" data-location="${location.name}">${_('Reset Student Attempts')}</a>
|
||||
|
|
||||
<a href="#" id="staff-debug-sdelete" data-location="${location.name}">${_('Delete Student State')}</a>
|
||||
|
|
||||
<a href="#" id="staff-debug-rescore" data-location="${location.name}">${_('Rescore Student Submission')}</a>
|
||||
|
||||
]
|
||||
</div>
|
||||
<div id="result_${location.name}"/>
|
||||
</div>
|
||||
|
||||
<div class="staff_info" style="display:block">
|
||||
is_released = ${is_released}
|
||||
location = ${location | h}
|
||||
|
||||
Reference in New Issue
Block a user