Refactor testing code, hook up frontend.
- now getting requests from js to server and back, with mocked service.
This commit is contained in:
@@ -222,24 +222,28 @@ class PageLoader(ActivateLoginTestCase):
|
||||
def check_for_get_code(self, code, url):
|
||||
"""
|
||||
Check that we got the expected code when accessing url via GET.
|
||||
Returns the response.
|
||||
"""
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}"
|
||||
.format(resp.status_code, url, code))
|
||||
return resp
|
||||
|
||||
|
||||
def check_for_post_code(self, code, url, data={}):
|
||||
"""
|
||||
Check that we got the expected code when accessing url via POST.
|
||||
Returns the response.
|
||||
"""
|
||||
resp = self.client.post(url, data)
|
||||
self.assertEqual(resp.status_code, code,
|
||||
"got code {0} for url '{1}'. Expected code {2}"
|
||||
.format(resp.status_code, url, code))
|
||||
return resp
|
||||
|
||||
|
||||
|
||||
|
||||
def check_pages_load(self, course_name, data_dir, modstore):
|
||||
"""Make all locations in course load"""
|
||||
print "Checking course {0} in {1}".format(course_name, data_dir)
|
||||
@@ -661,46 +665,46 @@ class TestCourseGrader(PageLoader):
|
||||
return [c for c in courses if c.id==course_id][0]
|
||||
|
||||
self.graded_course = find_course("edX/graded/2012_Fall")
|
||||
|
||||
|
||||
# create a test student
|
||||
self.student = 'view@test.com'
|
||||
self.password = 'foo'
|
||||
self.create_account('u1', self.student, self.password)
|
||||
self.activate_user(self.student)
|
||||
self.enroll(self.graded_course)
|
||||
|
||||
|
||||
self.student_user = user(self.student)
|
||||
|
||||
|
||||
self.factory = RequestFactory()
|
||||
|
||||
|
||||
def get_grade_summary(self):
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
return grades.grade(self.student_user, fake_request,
|
||||
self.graded_course, student_module_cache)
|
||||
|
||||
def get_homework_scores(self):
|
||||
return self.get_grade_summary()['totaled_scores']['Homework']
|
||||
|
||||
def get_progress_summary(self):
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
progress_summary = grades.progress_summary(self.student_user, fake_request,
|
||||
return grades.grade(self.student_user, fake_request,
|
||||
self.graded_course, student_module_cache)
|
||||
|
||||
def get_homework_scores(self):
|
||||
return self.get_grade_summary()['totaled_scores']['Homework']
|
||||
|
||||
def get_progress_summary(self):
|
||||
student_module_cache = StudentModuleCache.cache_for_descriptor_descendents(
|
||||
self.graded_course.id, self.student_user, self.graded_course)
|
||||
|
||||
fake_request = self.factory.get(reverse('progress',
|
||||
kwargs={'course_id': self.graded_course.id}))
|
||||
|
||||
progress_summary = grades.progress_summary(self.student_user, fake_request,
|
||||
self.graded_course, student_module_cache)
|
||||
return progress_summary
|
||||
|
||||
|
||||
def check_grade_percent(self, percent):
|
||||
grade_summary = self.get_grade_summary()
|
||||
self.assertEqual(percent, grade_summary['percent'])
|
||||
|
||||
self.assertEqual(grade_summary['percent'], percent)
|
||||
|
||||
def submit_question_answer(self, problem_url_name, responses):
|
||||
"""
|
||||
The field names of a problem are hard to determine. This method only works
|
||||
@@ -710,96 +714,96 @@ class TestCourseGrader(PageLoader):
|
||||
input_i4x-edX-graded-problem-H1P3_2_2
|
||||
"""
|
||||
problem_location = "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={
|
||||
'course_id' : self.graded_course.id,
|
||||
'location' : problem_location,
|
||||
'dispatch' : 'problem_check', }
|
||||
)
|
||||
|
||||
|
||||
resp = self.client.post(modx_url, {
|
||||
'input_i4x-edX-graded-problem-{0}_2_1'.format(problem_url_name): responses[0],
|
||||
'input_i4x-edX-graded-problem-{0}_2_2'.format(problem_url_name): responses[1],
|
||||
})
|
||||
print "modx_url" , modx_url, "responses" , responses
|
||||
print "resp" , resp
|
||||
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def problem_location(self, problem_url_name):
|
||||
return "i4x://edX/graded/problem/{0}".format(problem_url_name)
|
||||
|
||||
|
||||
def reset_question_answer(self, problem_url_name):
|
||||
problem_location = self.problem_location(problem_url_name)
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
|
||||
modx_url = reverse('modx_dispatch',
|
||||
kwargs={
|
||||
'course_id' : self.graded_course.id,
|
||||
'location' : problem_location,
|
||||
'dispatch' : 'problem_reset', }
|
||||
)
|
||||
|
||||
|
||||
resp = self.client.post(modx_url)
|
||||
return resp
|
||||
|
||||
return resp
|
||||
|
||||
def test_get_graded(self):
|
||||
#### Check that the grader shows we have 0% in the course
|
||||
self.check_grade_percent(0)
|
||||
|
||||
|
||||
#### Submit the answers to a few problems as ajax calls
|
||||
def earned_hw_scores():
|
||||
"""Global scores, each Score is a Problem Set"""
|
||||
return [s.earned for s in self.get_homework_scores()]
|
||||
|
||||
|
||||
def score_for_hw(hw_url_name):
|
||||
hw_section = [section for section
|
||||
in self.get_progress_summary()[0]['sections']
|
||||
if section.get('url_name') == hw_url_name][0]
|
||||
return [s.earned for s in hw_section['scores']]
|
||||
|
||||
|
||||
# Only get half of the first problem correct
|
||||
self.submit_question_answer('H1P1', ['Correct', 'Incorrect'])
|
||||
self.check_grade_percent(0.06)
|
||||
self.assertEqual(earned_hw_scores(), [1.0, 0, 0]) # Order matters
|
||||
self.assertEqual(score_for_hw('Homework1'), [1.0, 0.0])
|
||||
|
||||
|
||||
# Get both parts of the first problem correct
|
||||
self.reset_question_answer('H1P1')
|
||||
self.submit_question_answer('H1P1', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.13)
|
||||
self.assertEqual(earned_hw_scores(), [2.0, 0, 0])
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 0.0])
|
||||
|
||||
|
||||
# This problem is shown in an ABTest
|
||||
self.submit_question_answer('H1P2', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.25)
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 0.0, 0])
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
# This problem is hidden in an ABTest. Getting it correct doesn't change total grade
|
||||
self.submit_question_answer('H1P3', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.25)
|
||||
self.assertEqual(score_for_hw('Homework1'), [2.0, 2.0])
|
||||
|
||||
|
||||
# On the second homework, we only answer half of the questions.
|
||||
# Then it will be dropped when homework three becomes the higher percent
|
||||
# This problem is also weighted to be 4 points (instead of default of 2)
|
||||
# If the problem was unweighted the percent would have been 0.38 so we
|
||||
# If the problem was unweighted the percent would have been 0.38 so we
|
||||
# know it works.
|
||||
self.submit_question_answer('H2P1', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.42)
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0])
|
||||
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 0])
|
||||
|
||||
# Third homework
|
||||
self.submit_question_answer('H3P1', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.42) # Score didn't change
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0])
|
||||
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 2.0])
|
||||
|
||||
self.submit_question_answer('H3P2', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(0.5) # Now homework2 dropped. Score changes
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0])
|
||||
|
||||
self.assertEqual(earned_hw_scores(), [4.0, 4.0, 4.0])
|
||||
|
||||
# Now we answer the final question (worth half of the grade)
|
||||
self.submit_question_answer('FinalQuestion', ['Correct', 'Correct'])
|
||||
self.check_grade_percent(1.0) # Hooray! We got 100%
|
||||
|
||||
@@ -21,6 +21,25 @@ log = logging.getLogger("mitx.courseware")
|
||||
class GradingServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MockStaffGradingService(object):
|
||||
"""
|
||||
A simple mockup of a staff grading service, testing.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.cnt = 0
|
||||
|
||||
def get_next(self, course_id):
|
||||
self.cnt += 1
|
||||
return json.dumps({'success': True,
|
||||
'submission_id': self.cnt,
|
||||
'submission': 'Test submission {cnt}'.format(cnt=self.cnt),
|
||||
'rubric': 'A rubric'})
|
||||
|
||||
def save_grade(self, course_id, submission_id, score, feedback):
|
||||
return self.get_next(course_id)
|
||||
|
||||
|
||||
class StaffGradingService(object):
|
||||
"""
|
||||
Interface to staff grading backend.
|
||||
@@ -30,20 +49,20 @@ class StaffGradingService(object):
|
||||
# TODO: add auth
|
||||
self.session = requests.session()
|
||||
|
||||
def get_next(course_id):
|
||||
def get_next(self, course_id):
|
||||
"""
|
||||
Get the next thing to grade. Returns json, or raises GradingServiceError
|
||||
if there's a problem.
|
||||
"""
|
||||
try:
|
||||
r = self.session.get(url + 'get_next')
|
||||
r = self.session.get(self.url + 'get_next')
|
||||
except requests.exceptions.ConnectionError as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
def save_grade(course_id, submission_id, score, feedback):
|
||||
def save_grade(self, course_id, submission_id, score, feedback):
|
||||
"""
|
||||
Save a grade.
|
||||
|
||||
@@ -52,15 +71,15 @@ class StaffGradingService(object):
|
||||
Returns json, or raises GradingServiceError if there's a problem.
|
||||
"""
|
||||
try:
|
||||
r = self.session.get(url + 'save_grade')
|
||||
r = self.session.get(self.url + 'save_grade')
|
||||
except requests.exceptions.ConnectionError as err:
|
||||
# reraise as promised GradingServiceError, but preserve stacktrace.
|
||||
raise GradingServiceError, str(err), sys.exc_info()[2]
|
||||
|
||||
return r.text
|
||||
|
||||
_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL)
|
||||
|
||||
#_service = StaffGradingService(settings.STAFF_GRADING_BACKEND_URL)
|
||||
_service = MockStaffGradingService()
|
||||
|
||||
def _err_response(msg):
|
||||
"""
|
||||
|
||||
@@ -8,15 +8,24 @@ Notes for running by hand:
|
||||
django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/instructor
|
||||
"""
|
||||
|
||||
import courseware.tests.tests as ct
|
||||
|
||||
import json
|
||||
|
||||
from nose import SkipTest
|
||||
from mock import patch, Mock
|
||||
|
||||
from override_settings import override_settings
|
||||
|
||||
from django.contrib.auth.models import \
|
||||
Group # Need access to internal func to put users in the right group
|
||||
# Need access to internal func to put users in the right group
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django_comment_client.models import Role, FORUM_ROLE_ADMINISTRATOR, \
|
||||
FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_STUDENT
|
||||
from django_comment_client.utils import has_forum_access
|
||||
|
||||
from instructor import staff_grading_service
|
||||
from courseware.access import _course_staff_group_name
|
||||
import courseware.tests.tests as ct
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -79,7 +88,7 @@ class TestInstructorDashboardGradeDownloadCSV(ct.PageLoader):
|
||||
'''
|
||||
self.assertEqual(body, expected_body, msg)
|
||||
|
||||
|
||||
|
||||
FORUM_ROLES = [ FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA ]
|
||||
FORUM_ADMIN_ACTION_SUFFIX = { FORUM_ROLE_ADMINISTRATOR : 'admin', FORUM_ROLE_MODERATOR : 'moderator', FORUM_ROLE_COMMUNITY_TA : 'community TA'}
|
||||
FORUM_ADMIN_USER = { FORUM_ROLE_ADMINISTRATOR : 'forumadmin', FORUM_ROLE_MODERATOR : 'forummoderator', FORUM_ROLE_COMMUNITY_TA : 'forummoderator'}
|
||||
@@ -91,6 +100,8 @@ def action_name(operation, rolename):
|
||||
return '{0} forum {1}'.format(operation, FORUM_ADMIN_ACTION_SUFFIX[rolename])
|
||||
|
||||
|
||||
_mock_service = staff_grading_service.MockStaffGradingService()
|
||||
|
||||
@override_settings(MODULESTORE=ct.TEST_DATA_XML_MODULESTORE)
|
||||
class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
'''
|
||||
@@ -101,8 +112,15 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
<<<<<<< HEAD
|
||||
self.full = modulestore().get_course("edX/full/6.002_Spring_2012")
|
||||
self.toy = modulestore().get_course("edX/toy/2012_Fall")
|
||||
=======
|
||||
|
||||
|
||||
self.course_id = "edX/toy/2012_Fall"
|
||||
self.toy = modulestore().get_course(self.course_id)
|
||||
>>>>>>> Refactor testing code, hook up frontend.
|
||||
|
||||
# Create two accounts
|
||||
self.student = 'view@test.com'
|
||||
@@ -122,7 +140,7 @@ class TestInstructorDashboardForumAdmin(ct.PageLoader):
|
||||
self.enroll(self.toy)
|
||||
|
||||
|
||||
|
||||
|
||||
def initialize_roles(self, course_id):
|
||||
self.admin_role = Role.objects.get_or_create(name=FORUM_ROLE_ADMINISTRATOR, course_id=course_id)[0]
|
||||
self.moderator_role = Role.objects.get_or_create(name=FORUM_ROLE_MODERATOR, course_id=course_id)[0]
|
||||
@@ -220,7 +238,7 @@ class TestStaffGradingService(ct.PageLoader):
|
||||
'''
|
||||
|
||||
|
||||
|
||||
|
||||
def setUp(self):
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
|
||||
@@ -240,7 +258,6 @@ class TestStaffGradingService(ct.PageLoader):
|
||||
Make sure only staff have access.
|
||||
"""
|
||||
self.login(self.student, self.password)
|
||||
self.enroll(self.toy)
|
||||
|
||||
# both get and post should return 404
|
||||
for view_name in ('staff_grading_get_next', 'staff_grading_save_grade'):
|
||||
@@ -248,3 +265,32 @@ class TestStaffGradingService(ct.PageLoader):
|
||||
self.check_for_get_code(404, url)
|
||||
self.check_for_post_code(404, url)
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
@patch.object(staff_grading_service, '_service', _mock_service)
|
||||
def test_get_next(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id})
|
||||
|
||||
r = self.check_for_get_code(200, url)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'])
|
||||
self.assertEquals(d['submission_id'], _mock_service.cnt)
|
||||
|
||||
|
||||
@patch.object(staff_grading_service, '_service', _mock_service)
|
||||
def test_save_grade(self):
|
||||
self.login(self.instructor, self.password)
|
||||
|
||||
url = reverse('staff_grading_save_grade', kwargs={'course_id': self.course_id})
|
||||
|
||||
data = {'score': '12', 'feedback': 'great!', 'submission_id': '123'}
|
||||
r = self.check_for_post_code(200, url, data)
|
||||
d = json.loads(r.content)
|
||||
self.assertTrue(d['success'], str(d))
|
||||
self.assertEquals(d['submission_id'], _mock_service.cnt)
|
||||
|
||||
|
||||
>>>>>>> Refactor testing code, hook up frontend.
|
||||
|
||||
@@ -422,10 +422,15 @@ def staff_grading(request, course_id):
|
||||
|
||||
grading = StaffGrading(course)
|
||||
|
||||
ajax_url = reverse('staff_grading', kwargs={'course_id': course_id})
|
||||
if not ajax_url.endswith('/'):
|
||||
ajax_url += '/'
|
||||
|
||||
return render_to_response('instructor/staff_grading.html', {
|
||||
'view_html': grading.get_html(),
|
||||
'course': course,
|
||||
'course_id': course_id,
|
||||
'ajax_url': ajax_url,
|
||||
# Checked above
|
||||
'staff_access': True, })
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ class StaffGradingBackend
|
||||
success: true
|
||||
submission: 'submission! ' + @mock_cnt
|
||||
rubric: 'A rubric! ' + @mock_cnt
|
||||
submission_id: @mock_cnt
|
||||
|
||||
else if cmd == 'save_grade'
|
||||
console.log("eval: #{data.score} pts, Feedback: #{data.feedback}")
|
||||
@@ -31,6 +32,7 @@ class StaffGradingBackend
|
||||
success: true
|
||||
submission: 'another submission! ' + @mock_cnt
|
||||
rubric: 'A rubric!' + @mock_cnt
|
||||
submission_id: @mock_cnt
|
||||
else
|
||||
response =
|
||||
success: false
|
||||
@@ -74,6 +76,7 @@ class StaffGrading
|
||||
|
||||
# model state
|
||||
@state = state_no_data
|
||||
@submission_id = null
|
||||
@submission = ''
|
||||
@rubric = ''
|
||||
@error_msg = ''
|
||||
@@ -110,7 +113,7 @@ class StaffGrading
|
||||
|
||||
if response.success
|
||||
if response.submission
|
||||
@data_loaded(response.submission, response.rubric)
|
||||
@data_loaded(response.submission, response.rubric, response.submission_id)
|
||||
else
|
||||
@no_more(response.message)
|
||||
else
|
||||
@@ -122,7 +125,10 @@ class StaffGrading
|
||||
@backend.post('get_next', {}, @ajax_callback)
|
||||
|
||||
submit_and_get_next: () ->
|
||||
data = {score: @score, feedback: @feedback_area.val()}
|
||||
data =
|
||||
score: @score
|
||||
feedback: @feedback_area.val()
|
||||
submission_id: @submission_id
|
||||
|
||||
@backend.post('save_grade', data, @ajax_callback)
|
||||
|
||||
@@ -130,9 +136,10 @@ class StaffGrading
|
||||
@error_msg = msg
|
||||
@state = state_error
|
||||
|
||||
data_loaded: (submission, rubric) ->
|
||||
data_loaded: (submission, rubric, submission_id) ->
|
||||
@submission = submission
|
||||
@rubric = rubric
|
||||
@submission_id = submission_id
|
||||
@feedback_area.val('')
|
||||
@score = null
|
||||
@state = state_grading
|
||||
@@ -140,6 +147,7 @@ class StaffGrading
|
||||
no_more: (message) ->
|
||||
@submission = null
|
||||
@rubric = null
|
||||
@submission_id = null
|
||||
@message = message
|
||||
@state = state_no_data
|
||||
|
||||
@@ -196,7 +204,7 @@ class StaffGrading
|
||||
|
||||
|
||||
# for now, just create an instance and load it...
|
||||
mock_backend = true
|
||||
mock_backend = false
|
||||
ajax_url = $('.staff-grading').data('ajax_url')
|
||||
backend = new StaffGradingBackend(ajax_url, mock_backend)
|
||||
|
||||
|
||||
@@ -15,7 +15,44 @@
|
||||
</%block>
|
||||
|
||||
<section class="container">
|
||||
<div class="grading-wrapper">
|
||||
${view_html}
|
||||
</div>
|
||||
|
||||
<div class="staff-grading" data-ajax_url="${ajax_url}">
|
||||
<h1>Staff grading</h1>
|
||||
|
||||
<div class="error-container">
|
||||
</div>
|
||||
|
||||
<div class="message-container">
|
||||
</div>
|
||||
|
||||
<section class="submission-wrapper">
|
||||
<h3>Submission</h3>
|
||||
<div class="submission-container">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rubric-wrapper">
|
||||
<h3>Rubric</h3>
|
||||
<div class="rubric-container">
|
||||
</div>
|
||||
|
||||
<div class="evaluation">
|
||||
<textarea name="feedback" placeholder="Feedback for student..."
|
||||
class="feedback-area" cols="70" rows="10"></textarea>
|
||||
<p>
|
||||
<label for="correct-radio">Correct</label>
|
||||
<input type="radio" name="score-selection" id="correct-radio" value="1">
|
||||
<label for="incorrect-radio">Incorrect</label>
|
||||
<input type="radio" name="score-selection" id="incorrect-radio" value="0">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="submission">
|
||||
<input type="button" value="Submit" class="submit-button" name="show"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user