diff --git a/lms/djangoapps/instructor/staff_grading_service.py b/lms/djangoapps/instructor/staff_grading_service.py index b3b0f99e74..33c1d875af 100644 --- a/lms/djangoapps/instructor/staff_grading_service.py +++ b/lms/djangoapps/instructor/staff_grading_service.py @@ -29,16 +29,21 @@ class MockStaffGradingService(object): def __init__(self): self.cnt = 0 - def get_next(self, course_id, grader_id): + def get_next(self,course_id, location, grader_id): self.cnt += 1 return json.dumps({'success': True, 'submission_id': self.cnt, 'submission': 'Test submission {cnt}'.format(cnt=self.cnt), + 'num_graded': 3, + 'min_for_ml': 5, + 'num_pending': 4, + 'prompt': 'This is a fake prompt', + 'ml_error_info': 'ML info', 'max_score': 2 + self.cnt % 3, 'rubric': 'A rubric'}) def save_grade(self, course_id, grader_id, submission_id, score, feedback): - return self.get_next(course_id, grader_id) + return self.get_next(course_id, 'fake location', grader_id) class StaffGradingService(object): @@ -53,6 +58,7 @@ class StaffGradingService(object): self.login_url = self.url + '/login/' self.get_next_url = self.url + '/get_next_submission/' self.save_grade_url = self.url + '/save_grade/' + self.get_problem_list_url = self.url + '/get_problem_list/' self.session = requests.session() @@ -96,13 +102,43 @@ class StaffGradingService(object): return response + def get_problem_list(self, course_id, grader_id): + """ + Get the list of problems for a given course. - def get_next(self, course_id, grader_id): + Args: + course_id: course id that we want the problems of + grader_id: who is grading this? The anonymous user_id of the grader. + + Returns: + json string with the response from the service. (Deliberately not + writing out the fields here--see the docs on the staff_grading view + in the grading_controller repo) + + Raises: + GradingServiceError: something went wrong with the connection. + """ + op = lambda: self.session.get(self.get_problem_list_url, + allow_redirects = False, + params={'course_id': course_id, + 'grader_id': grader_id}) + try: + r = self._try_with_login(op) + except (RequestException, ConnectionError, HTTPError) as err: + # reraise as promised GradingServiceError, but preserve stacktrace. + raise GradingServiceError, str(err), sys.exc_info()[2] + + return r.text + + + def get_next(self, course_id, location, grader_id): """ Get the next thing to grade. Args: - course_id: course id to get submission for + course_id: the course that this problem belongs to + location: location of the problem that we are grading and would like the + next submission for grader_id: who is grading this? The anonymous user_id of the grader. Returns: @@ -115,7 +151,7 @@ class StaffGradingService(object): """ op = lambda: self.session.get(self.get_next_url, allow_redirects=False, - params={'course_id': course_id, + params={'location': location, 'grader_id': grader_id}) try: r = self._try_with_login(op) @@ -198,7 +234,8 @@ def _check_access(user, course_id): def get_next(request, course_id): """ - Get the next thing to grade for course_id. + Get the next thing to grade for course_id and with the location specified + in the . Returns a json dict with the following keys: @@ -218,17 +255,45 @@ def get_next(request, course_id): """ _check_access(request.user, course_id) - return HttpResponse(_get_next(course_id, request.user.id), + required = set(['location']) + if request.method != 'POST': + raise Http404 + actual = set(request.POST.keys()) + missing = required - actual + if len(missing) > 0: + return _err_response('Missing required keys {0}'.format( + ', '.join(missing))) + grader_id = request.user.id + p = request.POST + location = p['location'] + + return HttpResponse(_get_next(course_id, request.user.id, location), mimetype="application/json") -def _get_next(course_id, grader_id): +def get_problem_list(request, course_id): + """ + Get all the problems for the given course id + TODO: fill in all of this stuff + """ + _check_access(request.user, course_id) + try: + response = grading_service().get_problem_list(course_id, request.user.id) + return HttpResponse(response, + mimetype="application/json") + except GradingServiceError: + log.exception("Error from grading service. server url: {0}" + .format(grading_service().url)) + return HttpResponse(json.dumps({'success': False, + 'error': 'Could not connect to grading service'})) + + +def _get_next(course_id, grader_id, location): """ Implementation of get_next (also called from save_grade) -- returns a json string """ - try: - return grading_service().get_next(course_id, grader_id) + return grading_service().get_next(course_id, location, grader_id) except GradingServiceError: log.exception("Error from grading service. server url: {0}" .format(grading_service().url)) @@ -255,15 +320,17 @@ def save_grade(request, course_id): if request.method != 'POST': raise Http404 - required = set('score', 'feedback', 'submission_id') + required = set(['score', 'feedback', 'submission_id', 'location']) actual = set(request.POST.keys()) + log.debug(actual) missing = required - actual - if len(missing) != 0: + if len(missing) > 0: return _err_response('Missing required keys {0}'.format( ', '.join(missing))) grader_id = request.user.id p = request.POST + location = p['location'] try: result_json = grading_service().save_grade(course_id, @@ -286,6 +353,6 @@ def save_grade(request, course_id): return _err_response('Grading service failed') # Ok, save_grade seemed to work. Get the next submission to grade. - return HttpResponse(_get_next(course_id, grader_id), + return HttpResponse(_get_next(course_id, grader_id, location), mimetype="application/json") diff --git a/lms/djangoapps/instructor/tests.py b/lms/djangoapps/instructor/tests.py index c47eb170fc..4b5ab7e309 100644 --- a/lms/djangoapps/instructor/tests.py +++ b/lms/djangoapps/instructor/tests.py @@ -236,6 +236,7 @@ class TestStaffGradingService(ct.PageLoader): self.student = 'view@test.com' self.instructor = 'view2@test.com' self.password = 'foo' + self.location = 'TestLocation' self.create_account('u1', self.student, self.password) self.create_account('u2', self.instructor, self.password) self.activate_user(self.student) @@ -271,8 +272,9 @@ class TestStaffGradingService(ct.PageLoader): self.login(self.instructor, self.password) url = reverse('staff_grading_get_next', kwargs={'course_id': self.course_id}) + data = {'location': self.location} - r = self.check_for_get_code(200, url) + r = self.check_for_post_code(200, url, data) d = json.loads(r.content) self.assertTrue(d['success']) self.assertEquals(d['submission_id'], self.mock_service.cnt) @@ -285,7 +287,8 @@ class TestStaffGradingService(ct.PageLoader): data = {'score': '12', 'feedback': 'great!', - 'submission_id': '123'} + 'submission_id': '123', + 'location': self.location} r = self.check_for_post_code(200, url, data) d = json.loads(r.content) self.assertTrue(d['success'], str(d)) diff --git a/lms/static/coffee/src/staff_grading/staff_grading.coffee b/lms/static/coffee/src/staff_grading/staff_grading.coffee index f4831e35c4..abcf99e40b 100644 --- a/lms/static/coffee/src/staff_grading/staff_grading.coffee +++ b/lms/static/coffee/src/staff_grading/staff_grading.coffee @@ -21,34 +21,72 @@ class StaffGradingBackend # should take a location as an argument if cmd == 'get_next' @mock_cnt++ - response = - success: true - problem_name: 'Problem 1' - num_left: 3 - num_total: 5 - prompt: 'This is a fake prompt' - submission: 'submission! ' + @mock_cnt - rubric: 'A rubric! ' + @mock_cnt - submission_id: @mock_cnt - max_score: 2 + @mock_cnt % 3 - ml_error_info : 'ML accuracy info: ' + @mock_cnt + switch data.location + when 'i4x://MITx/3.091x/problem/open_ended_demo1' + response = + success: true + problem_name: 'Problem 1' + num_graded: 3 + min_for_ml: 5 + num_pending: 4 + prompt: ''' +

S11E3: Metal Bands

+

Shown below are schematic band diagrams for two different metals. Both diagrams appear different, yet both of the elements are undisputably metallic in nature.

+ +

* Why is it that both sodium and magnesium behave as metals, even though the s-band of magnesium is filled?

+

This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.

+ ''' + submission: ''' + Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32. + +The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. + ''' + rubric: ''' + + +

Please score your response according to how many of the above components you identified:

+ ''' + submission_id: @mock_cnt + max_score: 2 + @mock_cnt % 3 + ml_error_info : 'ML accuracy info: ' + @mock_cnt + when 'i4x://MITx/3.091x/problem/open_ended_demo2' + response = + success: true + problem_name: 'Problem 2' + num_graded: 2 + min_for_ml: 5 + num_pending: 4 + prompt: 'This is a fake second problem' + submission: 'This is the best submission ever! ' + @mock_cnt + rubric: 'I am a rubric for grading things! ' + @mock_cnt + submission_id: @mock_cnt + max_score: 2 + @mock_cnt % 3 + ml_error_info : 'ML accuracy info: ' + @mock_cnt + else + response = + success: false + else if cmd == 'save_grade' console.log("eval: #{data.score} pts, Feedback: #{data.feedback}") response = - @mock('get_next', {}) - # get_probblem_list - # sends in a course_id and a grader_id - # should get back a list of problem_ids, problem_names, num_left, num_total + @mock('get_next', {location: data.location}) + # get_problem_list + # should get back a list of problem_ids, problem_names, num_graded, min_for_ml else if cmd == 'get_problem_list' - response = - success: true - problem_list: [ - {location: 'i4x://MITx/3.091x/problem/open_ended_demo', \ - problem_name: "Problem 1", num_left: 3, num_total: 5}, - {location: 'i4x://MITx/3.091x/problem/open_ended_demo', \ - problem_name: "Problem 2", num_left: 1, num_total: 5} - ] + @mock_cnt = 1 + response = + success: true + problem_list: [ + {location: 'i4x://MITx/3.091x/problem/open_ended_demo1', \ + problem_name: "Problem 1", num_graded: 3, num_pending: 5, min_for_ml: 10}, + {location: 'i4x://MITx/3.091x/problem/open_ended_demo2', \ + problem_name: "Problem 2", num_graded: 1, num_pending: 5, min_for_ml: 10} + ] else response = success: false @@ -81,9 +119,14 @@ class StaffGrading @backend = backend # all the jquery selectors + + @problem_list_container = $('.problem-list-container') + @problem_list = $('.problem-list') + @error_container = $('.error-container') @message_container = $('.message-container') + @prompt_name_container = $('.prompt-name') @prompt_container = $('.prompt-container') @prompt_wrapper = $('.prompt-wrapper') @@ -92,11 +135,18 @@ class StaffGrading @rubric_container = $('.rubric-container') @rubric_wrapper = $('.rubric-wrapper') + @grading_wrapper = $('.grading-wrapper') @feedback_area = $('.feedback-area') @score_selection_container = $('.score-selection-container') @submit_button = $('.submit-button') + @action_button = $('.action-button') + + @problem_meta_info = $('.problem-meta-info-container') + @meta_info_wrapper = $('.meta-info-wrapper') @ml_error_info_container = $('.ml-error-info-container') + + @breadcrumbs = $('.breadcrumbs') # model state @state = state_no_data @@ -108,17 +158,22 @@ class StaffGrading @message = '' @max_score = 0 @ml_error_info= '' + @location = '' + @prompt_name = '' + @min_for_ml = 0 + @num_graded = 0 + @num_pending = 0 @score = null + @problems = null # action handlers @submit_button.click @submit - - # render intial state - @render_view() + # TODO: fix this to do something more intelligent + @action_button.click @submit # send initial request automatically - @get_next_submission() + @get_problem_list() setup_score_selection: => @@ -140,11 +195,12 @@ class StaffGrading set_button_text: (text) => - @submit_button.attr('value', text) + @action_button.attr('value', text) graded_callback: (event) => @score = event.target.value @state = state_graded + @message = '' @render_view() ajax_callback: (response) => @@ -153,8 +209,10 @@ class StaffGrading @message = '' if response.success - if response.submission - @data_loaded(response.prompt, response.submission, response.rubric, response.submission_id, response.max_score, response.ml_error_info) + if response.problem_list + @problems = response.problem_list + else if response.submission + @data_loaded(response) else @no_more(response.message) else @@ -162,14 +220,21 @@ class StaffGrading @render_view() - get_next_submission: () -> - @backend.post('get_next', {}, @ajax_callback) + get_next_submission: (location) -> + @location = location + @list_view = false + @backend.post('get_next', {location: location}, @ajax_callback) + + get_problem_list: () -> + @list_view = true + @backend.post('get_problem_list', {}, @ajax_callback) submit_and_get_next: () -> data = score: @score feedback: @feedback_area.val() submission_id: @submission_id + location: @location @backend.post('save_grade', data, @ajax_callback) @@ -177,21 +242,28 @@ class StaffGrading @error_msg = msg @state = state_error - data_loaded: (prompt, submission, rubric, submission_id, max_score, ml_error_info) -> - @prompt = prompt - @submission = submission - @rubric = rubric - @submission_id = submission_id + data_loaded: (response) -> + @prompt = response.prompt + @submission = response.submission + @rubric = response.rubric + @submission_id = response.submission_id @feedback_area.val('') - @max_score = max_score + @max_score = response.max_score @score = null - @ml_error_info=ml_error_info + @ml_error_info=response.ml_error_info + @prompt_name = response.problem_name + @num_graded = response.num_graded + @min_for_ml = response.min_for_ml + @num_pending = response.num_pending @state = state_grading if not @max_score? @error("No max score specified for submission.") no_more: (message) -> @prompt = null + @prompt_name = '' + @num_graded = 0 + @min_for_ml = 0 @submission = null @rubric = null @ml_error_info = null @@ -201,35 +273,92 @@ class StaffGrading @max_score = 0 @state = state_no_data - render_view: () -> - # make the view elements match the state. Idempotent. - show_grading_elements = false - show_submit_button = true - @message_container.html(@message) + render_view: () -> + # clear the problem list and breadcrumbs + @problem_list.html('') + @breadcrumbs.html('') + @problem_list_container.toggle(@list_view) if @backend.mock_backend - @message_container.append("

NOTE: Mocking backend.

") - + @message = @message + "

NOTE: Mocking backend.

" + @message_container.html(@message) @error_container.html(@error_msg) + @message_container.toggle(@message != "") + @error_container.toggle(@error_msg != "") + + + # only show the grading elements when we are not in list view or the state + # is invalid + show_grading_elements = !(@list_view || @state == state_error || + @state == state_no_data) + @prompt_wrapper.toggle(show_grading_elements) + @submission_wrapper.toggle(show_grading_elements) + @rubric_wrapper.toggle(show_grading_elements) + @grading_wrapper.toggle(show_grading_elements) + @meta_info_wrapper.toggle(show_grading_elements) + @action_button.hide() + + if @list_view + @render_list() + else + @render_problem() + + problem_link:(problem) -> + link = $('').attr('href', "javascript:void(0)").append( + "#{problem.problem_name} (#{problem.num_graded} graded, #{problem.num_pending} pending)") + .click => + @get_next_submission problem.location + + make_paragraphs: (text) -> + paragraph_split = text.split(/\n\s*\n/) + new_text = '' + for paragraph in paragraph_split + new_text += "

#{paragraph}

" + return new_text + + render_list: () -> + for problem in @problems + @problem_list.append($('
  • ').append(@problem_link(problem))) + + render_problem: () -> + # make the view elements match the state. Idempotent. + show_submit_button = true + show_action_button = true + + problem_list_link = $('').attr('href', 'javascript:void(0);') + .append("< Back to problem list") + .click => @get_problem_list() + + # set up the breadcrumbing + @breadcrumbs.append(problem_list_link) + if @state == state_error @set_button_text('Try loading again') + show_action_button = true else if @state == state_grading @ml_error_info_container.html(@ml_error_info) + meta_list = $("