mock_xqueue_server.py 6.73 KB
Newer Older
1 2 3 4
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
import json
import urllib
import urlparse
5
import threading
6

7 8
from logging import getLogger
logger = getLogger(__name__)
9

Will Daly committed
10

11 12 13 14 15 16 17 18 19 20 21 22
class MockXQueueRequestHandler(BaseHTTPRequestHandler):
    '''
    A handler for XQueue POST requests.
    '''

    protocol = "HTTP/1.0"

    def do_HEAD(self):
        self._send_head()

    def do_POST(self):
        '''
23
        Handle a POST request from the client
24 25

        Sends back an immediate success/failure response.
26
        It then POSTS back to the client
27 28 29 30 31 32 33
        with grading results, as configured in MockXQueueServer.
        '''
        self._send_head()

        # Retrieve the POST data
        post_dict = self._post_dict()

34
        # Log the request
Will Daly committed
35
        logger.debug("XQueue received POST request %s to path %s" %
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
                    (str(post_dict), self.path))

        # Respond only to grading requests
        if self._is_grade_request():
            try:
                xqueue_header = json.loads(post_dict['xqueue_header'])
                xqueue_body = json.loads(post_dict['xqueue_body'])

                callback_url = xqueue_header['lms_callback_url']

            except KeyError:
                # If the message doesn't have a header or body,
                # then it's malformed.
                # Respond with failure
                error_msg = "XQueue received invalid grade request"
                self._send_immediate_response(False, message=error_msg)

            except ValueError:
                # If we could not decode the body or header,
                # respond with failure
Will Daly committed
56

57 58 59 60
                error_msg = "XQueue could not decode grade request"
                self._send_immediate_response(False, message=error_msg)

            else:
Will Daly committed
61
                # Send an immediate response of success
62 63 64 65 66 67 68 69
                # The grade request is formed correctly
                self._send_immediate_response(True)

                # Wait a bit before POSTing back to the callback url with the
                # grade result configured by the server
                # Otherwise, the problem will not realize it's
                # queued and it will keep waiting for a response
                # indefinitely
Will Daly committed
70
                delayed_grade_func = lambda: self._send_grade_response(callback_url,
71 72 73 74 75 76 77 78 79 80
                                                                        xqueue_header)

                timer = threading.Timer(2, delayed_grade_func)
                timer.start()

        # If we get a request that's not to the grading submission
        # URL, return an error
        else:
            error_message = "Invalid request URL"
            self._send_immediate_response(False, message=error_message)
81 82 83 84 85 86


    def _send_head(self):
        '''
        Send the response code and MIME headers
        '''
87
        if self._is_grade_request():
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
            self.send_response(200)
        else:
            self.send_response(500)

        self.send_header('Content-type', 'text/plain')
        self.end_headers()

    def _post_dict(self):
        '''
        Retrieve the POST parameters from the client as a dictionary
        '''

        try:
            length = int(self.headers.getheader('content-length'))

            post_dict = urlparse.parse_qs(self.rfile.read(length))

            # The POST dict will contain a list of values
            # for each key.
            # None of our parameters are lists, however,
            # so we map [val] --> val
            # If the list contains multiple entries,
            # we pick the first one
            post_dict = dict(map(lambda (key, list_val): (key, list_val[0]),
                                post_dict.items()))

        except:
            # We return an empty dict here, on the assumption
            # that when we later check that the request has
            # the correct fields, it won't find them,
            # and will therefore send an error response
            return {}

        return post_dict

123
    def _send_immediate_response(self, success, message=""):
124
        '''
125 126
        Send an immediate success/failure message
        back to the client
127 128 129 130
        '''

        # Send the response indicating success/failure
        response_str = json.dumps({'return_code': 0 if success else 1,
131
                                'content': message})
132

133 134
        # Log the response
        logger.debug("XQueue: sent response %s" % response_str)
135

136
        self.wfile.write(response_str)
137

138
    def _send_grade_response(self, postback_url, xqueue_header):
139 140 141 142
        '''
        POST the grade response back to the client
        using the response provided by the server configuration
        '''
143 144
        response_dict = {'xqueue_header': json.dumps(xqueue_header),
                        'xqueue_body': json.dumps(self.server.grade_response())}
145

146 147
        # Log the response
        logger.debug("XQueue: sent grading response %s" % str(response_dict))
148

149
        MockXQueueRequestHandler.post_to_url(postback_url, response_dict)
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169

    def _is_grade_request(self):
        return 'xqueue/submit' in self.path

    @staticmethod
    def post_to_url(url, param_dict):
        '''
        POST *param_dict* to *url*
        We make this a separate function so we can easily patch
        it during testing.
        '''
        urllib.urlopen(url, urllib.urlencode(param_dict))


class MockXQueueServer(HTTPServer):
    '''
    A mock XQueue grading server that responds
    to POST requests to localhost.
    '''

Will Daly committed
170 171
    def __init__(self, port_num,
            grade_response_dict={'correct': True, 'score': 1, 'msg': ''}):
172 173 174 175 176 177 178 179 180
        '''
        Initialize the mock XQueue server instance.

        *port_num* is the localhost port to listen to

        *grade_response_dict* is a dictionary that will be JSON-serialized
            and sent in response to XQueue grading requests.
        '''

181
        self.set_grade_response(grade_response_dict)
182 183 184 185 186

        handler = MockXQueueRequestHandler
        address = ('', port_num)
        HTTPServer.__init__(self, address, handler)

187 188 189 190 191 192 193 194 195 196
    def shutdown(self):
        '''
        Stop the server and free up the port
        '''
        # First call superclass shutdown()
        HTTPServer.shutdown(self)

        # We also need to manually close the socket
        self.socket.close()

197 198 199
    def grade_response(self):
        return self._grade_response

200 201 202 203 204 205 206 207 208 209 210
    def set_grade_response(self, grade_response_dict):

        # Check that the grade response has the right keys
        assert('correct' in grade_response_dict and
                'score' in grade_response_dict and
                'msg' in grade_response_dict)

        # Wrap the message in <div> tags to ensure that it is valid XML
        grade_response_dict['msg'] = "<div>%s</div>" % grade_response_dict['msg']

        # Save the response dictionary
211
        self._grade_response = grade_response_dict