diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py index eb026e9c6b..480a119b48 100644 --- a/lms/djangoapps/courseware/tests/tests.py +++ b/lms/djangoapps/courseware/tests/tests.py @@ -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% diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index e4f82cd5e0..2a2a7a3552 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -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): """ diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index 5f740b8ff9..28a56ad9de 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -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. diff --git a/lms/djangoapps/instructor/views.py b/lms/djangoapps/instructor/views.py index b877a7236d..389a64721a 100644 --- a/lms/djangoapps/instructor/views.py +++ b/lms/djangoapps/instructor/views.py @@ -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, }) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index e410a3c41d..7440277d98 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -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) diff --git a/lms/templates/instructor/staff_grading.html b/lms/templates/instructor/staff_grading.html index d9d183c161..d8e834d4f6 100644 --- a/lms/templates/instructor/staff_grading.html +++ b/lms/templates/instructor/staff_grading.html @@ -15,7 +15,44 @@
-
- ${view_html} -
+ +
+

Staff grading

+ +
+
+ +
+
+ +
+

Submission

+
+
+
+ +
+

Rubric

+
+
+ +
+ +

+ + + + +

+
+ +
+ +
+ +
+ +
+