staff_grading_service.py 11.4 KB
Newer Older
1 2 3 4 5
"""
This module provides views that proxy to the staff grading backend service.
"""

import json
6
import logging
7
import requests
8
from requests.exceptions import RequestException, ConnectionError, HTTPError
9
import sys
10
from xmodule.grading_service_module import GradingService, GradingServiceError
11

12
from django.conf import settings
13
from django.http import HttpResponse, Http404
14

15
from courseware.access import has_access
16
from util.json_request import expect_json
17
from xmodule.course_module import CourseDescriptor
18
from student.models import unique_id_for_user
19 20
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
21

Victor Shnayder committed
22
log = logging.getLogger(__name__)
23

Calen Pennington committed
24

25 26 27 28 29 30 31
class MockStaffGradingService(object):
    """
    A simple mockup of a staff grading service, testing.
    """
    def __init__(self):
        self.cnt = 0

Calen Pennington committed
32
    def get_next(self, course_id, location, grader_id):
33 34 35 36
        self.cnt += 1
        return json.dumps({'success': True,
                           'submission_id': self.cnt,
                           'submission': 'Test submission {cnt}'.format(cnt=self.cnt),
37 38 39 40 41
                           'num_graded': 3,
                           'min_for_ml': 5,
                           'num_pending': 4,
                           'prompt': 'This is a fake prompt',
                           'ml_error_info': 'ML info',
42
                           'max_score': 2 + self.cnt % 3,
43 44
                           'rubric': 'A rubric'})

Diana Huang committed
45 46 47 48
    def get_problem_list(self, course_id, grader_id):
        self.cnt += 1
        return json.dumps({'success': True,
        'problem_list': [
49
          json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1', 
Diana Huang committed
50
            'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5, 'min_for_ml': 10}),
51
          json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
Diana Huang committed
52 53 54 55
            'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5, 'min_for_ml': 10})
        ]})


56
    def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores):
57
        return self.get_next(course_id, 'fake location', grader_id)
58 59


60
class StaffGradingService(GradingService):
61 62 63
    """
    Interface to staff grading backend.
    """
64
    def __init__(self, config):
Calen Pennington committed
65
        config['system'] = ModuleSystem(None, None, None, render_to_string, None)
66
        super(StaffGradingService, self).__init__(config)
67 68
        self.get_next_url = self.url + '/get_next_submission/'
        self.save_grade_url = self.url + '/save_grade/'
69
        self.get_problem_list_url = self.url + '/get_problem_list/'
70
        self.get_notifications_url = self.url + "/get_notifications/"
71 72


73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
    def get_problem_list(self, course_id, grader_id):
        """
        Get the list of problems for a given course.

        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.
        """
Calen Pennington committed
89
        params = {'course_id': course_id, 'grader_id': grader_id}
90
        return self.get(self.get_problem_list_url, params)
91

92 93

    def get_next(self, course_id, location, grader_id):
94
        """
95 96 97
        Get the next thing to grade.

        Args:
98 99 100
            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
101 102 103 104 105 106 107 108 109
            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.
110
        """
111
        response = self.get(self.get_next_url,
112
                                      params={'location': location,
113
                                              'grader_id': grader_id})
114
        return json.dumps(self._render_rubric(response))
115

116

117
    def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores):
118
        """
119
        Save a score and feedback for a submission.
120

121 122 123 124
        Returns:
            json dict with keys
                'success': bool
                'error': error msg, if something went wrong.
125

126 127
        Raises:
            GradingServiceError if there's a problem connecting.
128
        """
129 130 131 132 133
        data = {'course_id': course_id,
                'submission_id': submission_id,
                'score': score,
                'feedback': feedback,
                'grader_id': grader_id,
134
                'skipped': skipped,
135 136
                'rubric_scores': rubric_scores,
                'rubric_scores_complete': True}
137

138
        return self.post(self.save_grade_url, data=data)
139

140 141 142 143 144 145
    def get_notifications(self, course_id):
        params = {'course_id': course_id}
        response = self.get(self.get_notifications_url, params)
        return response


146
# don't initialize until staff_grading_service() is called--means that just
Victor Shnayder committed
147 148 149
# importing this file doesn't create objects that may not have the right config
_service = None

150

151
def staff_grading_service():
Victor Shnayder committed
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
    """
    Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True,
    returns a mock one, otherwise a real one.

    Caches the result, so changing the setting after the first call to this
    function will have no effect.
    """
    global _service
    if _service is not None:
        return _service

    if settings.MOCK_STAFF_GRADING:
        _service = MockStaffGradingService()
    else:
        _service = StaffGradingService(settings.STAFF_GRADING_INTERFACE)

    return _service
169

Calen Pennington committed
170

171 172 173 174
def _err_response(msg):
    """
    Return a HttpResponse with a json dump with success=False, and the given error message.
    """
175 176
    return HttpResponse(json.dumps({'success': False, 'error': msg}),
                        mimetype="application/json")
177 178 179 180 181 182 183 184 185 186 187 188


def _check_access(user, course_id):
    """
    Raise 404 if user doesn't have staff access to course_id
    """
    course_location = CourseDescriptor.id_to_location(course_id)
    if not has_access(user, course_location, 'staff'):
        raise Http404

    return

189 190 191

def get_next(request, course_id):
    """
192
    Get the next thing to grade for course_id and with the location specified
Diana Huang committed
193
    in the request.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212

    Returns a json dict with the following keys:

    'success': bool

    'submission_id': a unique identifier for the submission, to be passed back
                     with the grade.

    'submission': the submission, rendered as read-only html for grading

    'rubric': the rubric, also rendered as html.

    'message': if there was no submission available, but nothing went wrong,
            there will be a message field.

    'error': if success is False, will have an error message with more info.
    """
    _check_access(request.user, course_id)

213
    required = set(['location'])
214 215 216 217
    if request.method != 'POST':
        raise Http404
    actual = set(request.POST.keys())
    missing = required - actual
218
    if len(missing) > 0:
219 220
        return _err_response('Missing required keys {0}'.format(
            ', '.join(missing)))
221
    grader_id = unique_id_for_user(request.user)
222
    p = request.POST
223
    location = p['location']
224

225
    return HttpResponse(_get_next(course_id, grader_id, location),
226
                        mimetype="application/json")
227 228


229
def get_problem_list(request, course_id):
230
    """
231
    Get all the problems for the given course id
Diana Huang committed
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249

    Returns a json dict with the following keys:
        success: bool

        problem_list: a list containing json dicts with the following keys:
            each dict represents a different problem in the course

            location: the location of the problem

            problem_name: the name of the problem

            num_graded: the number of responses that have been graded

            num_pending: the number of responses that are sitting in the queue

            min_for_ml: the number of responses that need to be graded before
                the ml can be run

250
    """
251 252
    _check_access(request.user, course_id)
    try:
253
        response = staff_grading_service().get_problem_list(course_id, unique_id_for_user(request.user))
254 255 256 257
        return HttpResponse(response,
                mimetype="application/json")
    except GradingServiceError:
        log.exception("Error from grading service.  server url: {0}"
258
                      .format(staff_grading_service().url))
259 260
        return HttpResponse(json.dumps({'success': False,
                           'error': 'Could not connect to grading service'}))
261

262 263 264 265 266

def _get_next(course_id, grader_id, location):
    """
    Implementation of get_next (also called from save_grade) -- returns a json string
    """
267
    try:
268
        return staff_grading_service().get_next(course_id, location, grader_id)
269
    except GradingServiceError:
270
        log.exception("Error from grading service.  server url: {0}"
271
                      .format(staff_grading_service().url))
272 273
        return json.dumps({'success': False,
                           'error': 'Could not connect to grading service'})
274 275 276 277 278


@expect_json
def save_grade(request, course_id):
    """
279 280
    Save the grade and feedback for a submission, and, if all goes well, return
    the next thing to grade.
281

282 283 284 285 286 287 288
    Expects the following POST parameters:
    'score': int
    'feedback': string
    'submission_id': int

    Returns the same thing as get_next, except that additional error messages
    are possible if something goes wrong with saving the grade.
289
    """
290 291 292 293 294
    _check_access(request.user, course_id)

    if request.method != 'POST':
        raise Http404

295
    required = set(['score', 'feedback', 'submission_id', 'location', 'rubric_scores[]'])
296 297
    actual = set(request.POST.keys())
    missing = required - actual
298
    if len(missing) > 0:
299 300
        return _err_response('Missing required keys {0}'.format(
            ', '.join(missing)))
301

302
    grader_id = unique_id_for_user(request.user)
303 304
    p = request.POST

305 306 307

    location = p['location']
    skipped =  'skipped' in p
308

309
    try:
310
        result_json = staff_grading_service().save_grade(course_id,
311
                                          grader_id,
312 313
                                          p['submission_id'],
                                          p['score'],
314
                                          p['feedback'],
315
                                          skipped,
316
                                          p.getlist('rubric_scores[]'))
317 318 319 320 321 322 323
    except GradingServiceError:
        log.exception("Error saving grade")
        return _err_response('Could not connect to grading service')

    try:
        result = json.loads(result_json)
    except ValueError:
324
        log.exception("save_grade returned broken json: %s", result_json)
325 326 327 328 329 330 331
        return _err_response('Grading service returned mal-formatted data.')

    if not result.get('success', False):
        log.warning('Got success=False from grading service.  Response: %s', result_json)
        return _err_response('Grading service failed')

    # Ok, save_grade seemed to work.  Get the next submission to grade.
332
    return HttpResponse(_get_next(course_id, grader_id, location),
333
                        mimetype="application/json")