From af06b0d65c1b3ae1f04be1d18c155677e7bf7922 Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Fri, 4 Aug 2017 16:24:38 +0530 Subject: Completely rewrite the server pool. It is now much simpler. There are no xmlrpc servers anymore instead the tornado server takes a post request to submit a job asynchronously and the results are added to a shared dictionary. A get request can be used to check the status of a submitted job. This allows the submission and checking of code to be completely async and will make the application a lot more scalable. --- yaksh/code_server.py | 246 ++++++++++++++++++++++------------------ yaksh/settings.py | 6 +- yaksh/tests/test_code_server.py | 131 ++++++++++----------- 3 files changed, 206 insertions(+), 177 deletions(-) (limited to 'yaksh') diff --git a/yaksh/code_server.py b/yaksh/code_server.py index 834c5af..6acce74 100644 --- a/yaksh/code_server.py +++ b/yaksh/code_server.py @@ -1,59 +1,34 @@ #!/usr/bin/env python -"""This server runs an HTTP server (using tornado) and several code servers -using XMLRPC that can be submitted code -and tests and returns the output. It *should* be run as root and will run as -the user 'nobody' so as to minimize any damange by errant code. This can be -configured by editing settings.py to run as many servers as desired. One can -also specify the ports on the command line. Here are examples:: +"""This server runs an HTTP server (using tornado) to which code can be +submitted for checking. This is asynchronous so once submitted the user can +check for the result. - $ sudo ./code_server.py - # Runs servers based on settings.py:SERVER_PORTS one server per port given. - -or:: - - $ sudo ./code_server.py 8001 8002 8003 8004 8005 - # Runs 5 servers on ports specified. - -All these servers should be running as nobody. This will also start a server -pool that defaults to port 50000 and is configurable in -settings.py:SERVER_POOL_PORT. This port exposes a `get_server_port` function -that returns an available server. +It *should* be run as root and will run as the user 'nobody' so as to minimize +any damange by errant code. This can be configured by editing settings.py to +run as many servers as desired. """ # Standard library imports from __future__ import unicode_literals +from argparse import ArgumentParser import json -from multiprocessing import Process, Queue +from multiprocessing import Process, Queue, Manager import os -from os.path import isdir, dirname, abspath, join, isfile +from os.path import dirname, abspath import pwd -import re -import signal -import stat -import subprocess import sys - -try: - from SimpleXMLRPCServer import SimpleXMLRPCServer -except ImportError: - # The above import will not work on Python-3.x. - from xmlrpc.server import SimpleXMLRPCServer - -try: - from urllib import unquote -except ImportError: - # The above import will not work on Python-3.x. - from urllib.parse import unquote +import time # Library imports +import requests from tornado.ioloop import IOLoop from tornado.web import Application, RequestHandler +from six.moves import urllib # Local imports -from .settings import SERVER_PORTS, SERVER_POOL_PORT -from .language_registry import create_evaluator_instance +from .settings import N_CODE_SERVERS, SERVER_POOL_PORT from .grader import Grader @@ -69,70 +44,45 @@ def run_as_nobody(): os.seteuid(nobody.pw_uid) -############################################################################### -# `CodeServer` class. -############################################################################### -class CodeServer(object): - """A code server that executes user submitted test code, tests it and - reports if the code was correct or not. +def check_code(job_queue, results): + """Check the code, this runs forever. """ - def __init__(self, port, queue): - self.port = port - self.queue = queue - - # Public Protocol ########## - def check_code(self, language, json_data, in_dir=None): - """Calls relevant EvaluateCode class based on language to check the - answer code - """ + while True: + uid, json_data, user_dir = job_queue.get(True) + results[uid] = dict(status='running', result=None) data = json.loads(json_data) - grader = Grader(in_dir) - result = grader.evaluate(data) - - # Put us back into the server pool queue since we are free now. - self.queue.put(self.port) - - return json.dumps(result) - - def run(self): - """Run XMLRPC server, serving our methods.""" - server = SimpleXMLRPCServer(("0.0.0.0", self.port)) - self.server = server - server.register_instance(self) - self.queue.put(self.port) - server.serve_forever() + grader = Grader(user_dir) + result = grader.evaluate(data) + results[uid] = dict(status='done', result=json.dumps(result)) ############################################################################### # `ServerPool` class. ############################################################################### class ServerPool(object): - """Manages a pool of CodeServer objects.""" - def __init__(self, ports, pool_port=50000): - """Create a pool of servers. Uses a shared Queue to get available - servers. + """Manages a pool of processes checking code.""" + def __init__(self, n, pool_port=50000): + """Create a pool of servers. Parameters ---------- - ports : list(int) - List of ports at which the CodeServer's should run. + n : int + Number of code servers to run pool_port : int Port at which the server pool should serve. """ + self.n = n + self.manager = Manager() + self.results = self.manager.dict() self.my_port = pool_port - self.ports = ports - queue = Queue(maxsize=len(self.ports)) - self.queue = queue - servers = [] + + self.job_queue = Queue() processes = [] - for port in self.ports: - server = CodeServer(port, queue) - servers.append(server) - p = Process(target=server.run) + for i in range(n): + p = Process(target=check_code, args=(self.job_queue, self.results)) processes.append(p) - self.servers = servers self.processes = processes self.app = self._make_app() @@ -150,21 +100,26 @@ class ServerPool(object): # Public Protocol ########## - def get_server_port(self): - """Get available server port from ones in the pool. This will block - till it gets an available server. - """ - return self.queue.get() - def get_status(self): - """Returns current queue size and total number of ports used.""" - try: - qs = self.queue.qsize() - except NotImplementedError: - # May not work on OS X so we return a dummy. - qs = len(self.ports) - - return qs, len(self.ports) + """Returns current job queue size, total number of processes alive. + """ + qs = sum(r['status'] == 'not started' + for r in self.results.values()) + alive = sum(p.is_alive() for p in self.processes) + n_running = sum(r['status'] == 'running' + for r in self.results.values()) + + return qs, alive, n_running + + def submit(self, uid, json_data, user_dir): + self.results[uid] = dict(status='not started') + self.job_queue.put((uid, json_data, user_dir)) + + def get_result(self, uid): + result = self.results.get(uid, dict(status='unknown')) + if result.get('status') == 'done': + self.results.pop(uid) + return json.dumps(result) def run(self): """Run server which returns an available server port where code @@ -189,24 +144,95 @@ class MainHandler(RequestHandler): def get(self): path = self.request.path[1:] if len(path) == 0: - port = self.server.get_server_port() - self.write(str(port)) - elif path == "status": - q_size, total = self.server.get_status() - result = "%d servers out of %d are free.\n"%(q_size, total) - load = float(total - q_size)/total*100 - result += "Load: %s%%\n"%load + q_size, alive, running = self.server.get_status() + result = "%d processes, %d running, %d queued" % ( + alive, running, q_size + ) self.write(result) + else: + uid = path + json_result = self.server.get_result(uid) + self.write(json_result) + + def post(self): + uid = self.get_argument('uid') + json_data = self.get_argument('json_data') + user_dir = self.get_argument('user_dir') + self.server.submit(uid, json_data, user_dir) + self.write('OK') + + +def submit(url, uid, json_data, user_dir): + '''Submit a job to the code server. + + Parameters + ---------- + + url : str + URL of the server pool. + + uid : str + Unique ID of the submission. + + json_data : jsonized str + Data to send to the code checker. + + user_dir : str + User directory. + ''' + requests.post( + url, data=dict(uid=uid, json_data=json_data, user_dir=user_dir) + ) + + +def get_result(url, uid, block=False): + '''Get the status of a job submitted to the code server. + + Returns the result currently known in the form of a dict. The dictionary + contains two keys, 'status' and 'result'. The status can be one of + ['running', 'not started', 'done', 'unknown']. The result is the result of + the code execution as a jsonized string. + + Parameters + ---------- + + url : str + URL of the server pool. + + uid : str + Unique ID of the submission. + + block : bool + Set to True if you wish to block till result is done. + + ''' + def _get_data(): + r = requests.get(urllib.parse.urljoin(url, str(uid))) + return json.loads(r.content) + data = _get_data() + if block: + while data.get('status') != 'done': + time.sleep(0.1) + data = _get_data() + + return data ############################################################################### def main(args=None): - if args: - ports = [int(x) for x in args] - else: - ports = SERVER_PORTS - - server_pool = ServerPool(ports=ports, pool_port=SERVER_POOL_PORT) + parser = ArgumentParser(description=__doc__) + parser.add_argument( + 'n', nargs='?', type=int, default=N_CODE_SERVERS, + help="Number of servers to run." + ) + parser.add_argument( + '-p', '--port', dest='port', default=SERVER_POOL_PORT, + help="Port at which the http server should run." + ) + + options = parser.parse_args(args) + + server_pool = ServerPool(n=options.n, pool_port=options.port) # This is done *after* the server pool is created because when the tornado # app calls listen(), it cannot be nobody. run_as_nobody() diff --git a/yaksh/settings.py b/yaksh/settings.py index 72f9fda..d500d93 100644 --- a/yaksh/settings.py +++ b/yaksh/settings.py @@ -1,9 +1,9 @@ """ settings for yaksh app. """ -# The ports the code server should run on. This will run one separate -# server for each port listed in the following list. -SERVER_PORTS = [8001] # range(8001, 8026) + +# The number of code server processes to run.. +N_CODE_SERVERS = 5 # The server pool port. This is the server which returns available server # ports so as to minimize load. This is some random number where no other diff --git a/yaksh/tests/test_code_server.py b/yaksh/tests/test_code_server.py index 47c1da7..a73c12f 100644 --- a/yaksh/tests/test_code_server.py +++ b/yaksh/tests/test_code_server.py @@ -8,9 +8,8 @@ from threading import Thread import unittest from six.moves import urllib -from yaksh.code_server import ServerPool, SERVER_POOL_PORT +from yaksh.code_server import ServerPool, SERVER_POOL_PORT, submit, get_result from yaksh import settings -from yaksh.xmlrpc_clients import CodeServerProxy class TestCodeServer(unittest.TestCase): @@ -19,8 +18,7 @@ class TestCodeServer(unittest.TestCase): def setUpClass(cls): settings.code_evaluators['python']['standardtestcase'] = \ "yaksh.python_assertion_evaluator.PythonAssertionEvaluator" - ports = range(8001, 8006) - server_pool = ServerPool(ports=ports, pool_port=SERVER_POOL_PORT) + server_pool = ServerPool(n=5, pool_port=SERVER_POOL_PORT) cls.server_pool = server_pool cls.server_thread = t = Thread(target=server_pool.run) t.start() @@ -33,70 +31,78 @@ class TestCodeServer(unittest.TestCase): "python_assertion_evaluator.PythonAssertionEvaluator" def setUp(self): - self.code_server = CodeServerProxy() + self.url = 'http://localhost:%s' % SERVER_POOL_PORT def test_infinite_loop(self): # Given - testdata = {'metadata': {'user_answer': 'while True: pass', - 'language': 'python', - 'partial_grading': False - }, - 'test_case_data': [{'test_case':'assert 1==2', - 'test_case_type': 'standardtestcase', - 'weight': 0.0 - }] - } + testdata = { + 'metadata': { + 'user_answer': 'while True: pass', + 'language': 'python', + 'partial_grading': False + }, + 'test_case_data': [ + {'test_case': 'assert 1==2', + 'test_case_type': 'standardtestcase', + 'weight': 0.0} + ] + } # When - result = self.code_server.run_code( - 'python', json.dumps(testdata), '' - ) + submit(self.url, '0', json.dumps(testdata), '') + result = get_result(self.url, '0') # Then - data = json.loads(result) + self.assertTrue(result.get('status') in ['running', 'not started']) + + # When + result = get_result(self.url, '0', block=True) + + # Then + data = json.loads(result.get('result')) self.assertFalse(data['success']) self.assertTrue('infinite loop' in data['error'][0]) def test_correct_answer(self): # Given - testdata = {'metadata': { 'user_answer': 'def f(): return 1', - 'language': 'python', - 'partial_grading': False - }, - 'test_case_data': [{'test_case':'assert f() == 1', - 'test_case_type': 'standardtestcase', - 'weight': 0.0 - }] - } + testdata = { + 'metadata': { + 'user_answer': 'def f(): return 1', + 'language': 'python', + 'partial_grading': False + }, + 'test_case_data': [{'test_case': 'assert f() == 1', + 'test_case_type': 'standardtestcase', + 'weight': 0.0}] + } # When - result = self.code_server.run_code( - 'python', json.dumps(testdata), '' - ) + submit(self.url, '0', json.dumps(testdata), '') + result = get_result(self.url, '0', block=True) # Then - data = json.loads(result) + data = json.loads(result.get('result')) self.assertTrue(data['success']) def test_wrong_answer(self): # Given - testdata = {'metadata': { 'user_answer': 'def f(): return 1', - 'language': 'python', - 'partial_grading': False - }, - 'test_case_data': [{'test_case':'assert f() == 2', - 'test_case_type': 'standardtestcase', - 'weight': 0.0 - }] - } + testdata = { + 'metadata': { + 'user_answer': 'def f(): return 1', + 'language': 'python', + 'partial_grading': False + }, + 'test_case_data': [{'test_case': 'assert f() == 2', + 'test_case_type': 'standardtestcase', + 'weight': 0.0}] + } # When - result = self.code_server.run_code( - 'python', json.dumps(testdata), '' - ) + submit(self.url, '0', json.dumps(testdata), '') + result = get_result(self.url, '0', block=True) # Then - data = json.loads(result) + data = json.loads(result.get('result')) self.assertFalse(data['success']) self.assertTrue('AssertionError' in data['error'][0]) @@ -104,28 +110,27 @@ class TestCodeServer(unittest.TestCase): # Given results = Queue() - def run_code(): + def run_code(uid): """Run an infinite loop.""" - testdata = {'metadata': { 'user_answer': 'while True: pass', - 'language': 'python', - 'partial_grading': False - }, - 'test_case_data': [{'test_case':'assert 1==2', - 'test_case_type': 'standardtestcase', - 'weight': 0.0 - }] - } - result = self.code_server.run_code( - 'python', json.dumps(testdata), '' - ) - results.put(json.loads(result)) + testdata = { + 'metadata': { + 'user_answer': 'while True: pass', + 'language': 'python', + 'partial_grading': False + }, + 'test_case_data': [{'test_case': 'assert 1==2', + 'test_case_type': 'standardtestcase', + 'weight': 0.0}] + } + submit(self.url, uid, json.dumps(testdata), '') + result = get_result(self.url, uid, block=True) + results.put(json.loads(result.get('result'))) N = 10 # When - import time threads = [] for i in range(N): - t = Thread(target=run_code) + t = Thread(target=run_code, args=(str(i),)) threads.append(t) t.start() @@ -142,16 +147,14 @@ class TestCodeServer(unittest.TestCase): def test_server_pool_status(self): # Given - url = "http://localhost:%s/status"%SERVER_POOL_PORT + url = "http://localhost:%s/" % SERVER_POOL_PORT # When response = urllib.request.urlopen(url) data = response.read().decode('utf-8') # Then - expect = 'out of 5 are free' - self.assertTrue(expect in data) - expect = 'Load:' + expect = '5 processes, 0 running, 0 queued' self.assertTrue(expect in data) -- cgit From 1a6006b457a68e6db51315a83a97ec0d9eb631d4 Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Wed, 9 Aug 2017 20:12:20 +0530 Subject: Fix order of run as nobody. It should be run before ServerPool is created so the shared dictionary works correctly. --- yaksh/code_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'yaksh') diff --git a/yaksh/code_server.py b/yaksh/code_server.py index 6acce74..7bacc7f 100644 --- a/yaksh/code_server.py +++ b/yaksh/code_server.py @@ -232,10 +232,10 @@ def main(args=None): options = parser.parse_args(args) - server_pool = ServerPool(n=options.n, pool_port=options.port) - # This is done *after* the server pool is created because when the tornado - # app calls listen(), it cannot be nobody. + # Called before serverpool is created so that the multiprocessing + # can work properly. run_as_nobody() + server_pool = ServerPool(n=options.n, pool_port=options.port) server_pool.run() -- cgit From ce995e06e3509a1340061c51dfa08a65c69eef66 Mon Sep 17 00:00:00 2001 From: prathamesh Date: Mon, 14 Aug 2017 12:10:42 +0530 Subject: Front-end modification for improved code server Added JQuery to handle request. Sends ajax request and gets json as response. json contains token i.e uid which is answer id. Using uid, an ajax request is sent after every 2 secs till the server gives the desire result. If the code result has error then html is written on the document. If the result has correct answer then next question is displayed. *includes function for string will not work for older browers. Will substitute with a different function in next commit. --- yaksh/code_server.py | 2 +- yaksh/models.py | 11 ++-- yaksh/static/yaksh/js/question.js | 12 ----- yaksh/static/yaksh/js/requesthandler.js | 90 +++++++++++++++++++++++++++++++++ yaksh/templates/yaksh/question.html | 51 +++---------------- yaksh/urls.py | 1 + yaksh/views.py | 79 +++++++++++++++++++---------- 7 files changed, 155 insertions(+), 91 deletions(-) mode change 100644 => 100755 yaksh/code_server.py delete mode 100644 yaksh/static/yaksh/js/question.js create mode 100644 yaksh/static/yaksh/js/requesthandler.js (limited to 'yaksh') diff --git a/yaksh/code_server.py b/yaksh/code_server.py old mode 100644 new mode 100755 index 7bacc7f..d74d35b --- a/yaksh/code_server.py +++ b/yaksh/code_server.py @@ -208,7 +208,7 @@ def get_result(url, uid, block=False): ''' def _get_data(): r = requests.get(urllib.parse.urljoin(url, str(uid))) - return json.loads(r.content) + return json.loads(r.content.decode('utf-8')) data = _get_data() if block: while data.get('status') != 'done': diff --git a/yaksh/models.py b/yaksh/models.py index 87e6260..9b3cabe 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -24,7 +24,7 @@ import zipfile import tempfile from textwrap import dedent from .file_utils import extract_files, delete_files -from yaksh.xmlrpc_clients import code_server +from yaksh.code_server import submit, SERVER_POOL_PORT from django.conf import settings @@ -1266,7 +1266,7 @@ class AnswerPaper(models.Model): if question.type == 'code': return self.answers.filter(question=question).order_by('-id') - def validate_answer(self, user_answer, question, json_data=None): + def validate_answer(self, user_answer, question, json_data=None, uid=None): """ Checks whether the answer submitted by the user is right or wrong. If right then returns correct = True, success and @@ -1327,10 +1327,9 @@ class AnswerPaper(models.Model): elif question.type == 'code' or question.type == "upload": user_dir = self.user.profile.get_user_dir() - json_result = code_server.run_code( - question.language, json_data, user_dir - ) - result = json.loads(json_result) + url = 'http://localhost:%s' % SERVER_POOL_PORT + submit(url, uid, json_data, user_dir) + result = {'uid': uid, 'state': 'running'} return result def regrade(self, question_id): diff --git a/yaksh/static/yaksh/js/question.js b/yaksh/static/yaksh/js/question.js deleted file mode 100644 index 96ff3de..0000000 --- a/yaksh/static/yaksh/js/question.js +++ /dev/null @@ -1,12 +0,0 @@ -function submitCode() -{ - document.forms["code"].submit(); - var x = document.getElementById("status"); - x.innerHTML = "Checking answer ..."; - x = document.getElementById("check"); - x.disabled = true; - x.value = "Checking Answer ..."; - if (document.getElementById("skip")!=null) { - document.getElementById("skip").disabled = true; - } -} diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js new file mode 100644 index 0000000..9d023cc --- /dev/null +++ b/yaksh/static/yaksh/js/requesthandler.js @@ -0,0 +1,90 @@ +request_status = "initial" +function submitRequest(){ + document.forms["code"].submit(); +} + +function check_state(state, uid, success) { + if (state == "running") { + setTimeout(get_result(uid), 2000); + } +} + +function get_result(uid){ + $.ajax({ + method: "GET", + url: "/exam/get_results/"+uid+"/", + dataType: "html", // Your server can response html, json, xml format. + success: function(data, status, xhr) { + content_type = xhr.getResponseHeader("content-type"); + if(content_type.includes("text/html")) { + request_status = "initial"; + document.open(); + document.write(data); + document.close(); + } else if(content_type.includes("application/json")) { + res = JSON.parse(data); + request_status = res.status; + check_state(request_status, uid, res.success); + } + } + }); +} + +var global_editor = {}; +$(document).ready(function(){ + // Codemirror object, language modes and initial content + // Get the textarea node + var textarea_node = document.querySelector('#answer'); + + var mode_dict = { + 'python': 'python', + 'c': 'text/x-csrc', + 'cpp': 'text/x-c++src', + 'java': 'text/x-java', + 'bash': 'text/x-sh', + 'scilab': 'text/x-csrc' + } + + // Code mirror Options + var options = { + mode: mode_dict[lang], + gutter: true, + lineNumbers: true, + onChange: function (instance, changes) { + render(); + } + }; + + // Initialize the codemirror editor + global_editor.editor = CodeMirror.fromTextArea(textarea_node, options); + + // Setting code editors initial content + global_editor.editor.setValue(init_val); + + function reset_editor() { + global_editor.editor.setValue(init_val); + global_editor.editor.clearHistory(); + } + $('#code').submit(function(e) { + $.ajax({ + type: 'POST', + url: $(this).attr("action"), + data: $(this).serializeArray(), + dataType: "html", // Your server can response html, json, xml format. + success: function(data, status, xhr) { + content_type = xhr.getResponseHeader("content-type"); + if(content_type.includes("text/html")) { + document.open(); + document.write(data); + document.close(); + } else if(content_type.includes("application/json")) { + res = JSON.parse(data); + var uid = res.uid; + request_status = res.state; + check_state(request_status, uid, false); + } + } + }); + e.preventDefault(); // To stop the default form submission. + }); +}); diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index ee33523..9df2fef 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -15,7 +15,7 @@ {% endblock %} {% block script %} - + @@ -79,7 +79,7 @@ function validate(){ } else { - return true; + send_request(); } } @@ -89,47 +89,8 @@ function call_skip(url) form.action = url form.submit(); } - - {% endblock script %} @@ -239,13 +200,13 @@ function call_skip(url)
{% if question.type == "mcq" or "mcc" or "integer" or "float" or "string" %} -
  
+
  
{% elif question.type == "upload" %}
   {% else %} {% if question in paper.get_questions_unanswered %} -    +    {% endif %} {% endif %} diff --git a/yaksh/urls.py b/yaksh/urls.py index 5270068..6daaf46 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ views.complete), url(r'^register/$', views.user_register, name="register"), url(r'^(?P\d+)/check/$', views.check), + url(r'^get_results/(?P\d+)/$', views.get_results), url(r'^(?P\d+)/check/(?P\d+)/(?P\d+)/$',\ views.check), url(r'^(?P\d+)/skip/(?P\d+)/(?P\d+)/$', diff --git a/yaksh/views.py b/yaksh/views.py index c10ba6a..f6243a7 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -4,7 +4,7 @@ import os from datetime import datetime, timedelta import collections import csv -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django.core.urlresolvers import reverse from django.contrib.auth import login, logout, authenticate from django.shortcuts import render_to_response, get_object_or_404, redirect @@ -30,6 +30,7 @@ try: except ImportError: from io import BytesIO as string_io # Local imports. +from yaksh.code_server import get_result, SERVER_POOL_PORT from yaksh.models import get_model_class, Quiz, Question, QuestionPaper, QuestionSet, Course from yaksh.models import Profile, Answer, AnswerPaper, User, TestCase, FileUpload,\ has_profile, StandardTestCase, McqTestCase,\ @@ -539,6 +540,7 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None): new_answer = Answer(question=current_question, answer=user_answer, correct=False, error=json.dumps([])) new_answer.save() + uid = new_answer.id paper.answers.add(new_answer) # If we were not skipped, we were asked to check. For any non-mcq # questions, we obtain the results via XML-RPC with the code executed @@ -547,38 +549,61 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None): if current_question.type == 'code' or \ current_question.type == 'upload' else None result = paper.validate_answer(user_answer, current_question, - json_data + json_data, uid ) - if result.get('success'): - new_answer.marks = (current_question.points * result['weight'] / - current_question.get_maximum_test_case_weight()) \ - if current_question.partial_grading and \ - current_question.type == 'code' or current_question.type == 'upload' \ - else current_question.points - new_answer.correct = result.get('success') - error_message = None - new_answer.error = json.dumps(result.get('error')) - next_question = paper.add_completed_question(current_question.id) + if current_question.type in ['code', 'upload']: + return JsonResponse(result) else: - new_answer.marks = (current_question.points * result['weight'] / - current_question.get_maximum_test_case_weight()) \ - if current_question.partial_grading and \ - current_question.type == 'code' or current_question.type == 'upload' \ - else 0 - error_message = result.get('error') if current_question.type == 'code' \ - or current_question.type == 'upload' else None - new_answer.error = json.dumps(result.get('error')) - next_question = current_question if current_question.type == 'code' \ - or current_question.type == 'upload' \ - else paper.add_completed_question(current_question.id) - new_answer.save() - paper.update_marks('inprogress') - paper.set_end_time(timezone.now()) - return show_question(request, next_question, paper, error_message) + next_question, error_message, paper = _update_paper(request, uid, result) + return show_question(request, next_question, paper, error_message) else: return show_question(request, current_question, paper) +@csrf_exempt +def get_results(request, uid): + url = 'http://localhost:%s' % SERVER_POOL_PORT + result_state = get_result(url, uid) + result = json.loads(result_state.get('result')) + next_question, error_message, paper = _update_paper(request, uid, result) + result['status'] = result_state.get('status') + if result['status']== 'done': + return show_question(request, next_question, paper, error_message) + return JsonResponse(result) + + +def _update_paper(request, uid, result): + new_answer = Answer.objects.get(id=uid) + current_question = new_answer.question + paper = new_answer.answerpaper_set.first() + + if result.get('success'): + new_answer.marks = (current_question.points * result['weight'] / + current_question.get_maximum_test_case_weight()) \ + if current_question.partial_grading and \ + current_question.type == 'code' or current_question.type == 'upload' \ + else current_question.points + new_answer.correct = result.get('success') + error_message = None + new_answer.error = json.dumps(result.get('error')) + next_question = paper.add_completed_question(current_question.id) + else: + new_answer.marks = (current_question.points * result['weight'] / + current_question.get_maximum_test_case_weight()) \ + if current_question.partial_grading and \ + current_question.type == 'code' or current_question.type == 'upload' \ + else 0 + error_message = result.get('error') if current_question.type == 'code' \ + or current_question.type == 'upload' else None + new_answer.error = json.dumps(result.get('error')) + next_question = current_question if current_question.type == 'code' \ + or current_question.type == 'upload' \ + else paper.add_completed_question(current_question.id) + new_answer.save() + paper.update_marks('inprogress') + paper.set_end_time(timezone.now()) + return next_question, error_message, paper + def quit(request, reason=None, attempt_num=None, questionpaper_id=None): """Show the quit page when the user logs out.""" -- cgit From 49615e5a24ecfdd0b22bae080e7f9bb2507bbfd7 Mon Sep 17 00:00:00 2001 From: prathamesh Date: Mon, 14 Aug 2017 14:51:39 +0530 Subject: To handle unknown status response from code server --- yaksh/static/yaksh/js/requesthandler.js | 15 +++++++++++---- yaksh/templates/yaksh/question.html | 7 +++++-- yaksh/views.py | 7 ++++--- 3 files changed, 20 insertions(+), 9 deletions(-) (limited to 'yaksh') diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js index 9d023cc..b637928 100644 --- a/yaksh/static/yaksh/js/requesthandler.js +++ b/yaksh/static/yaksh/js/requesthandler.js @@ -3,9 +3,15 @@ function submitRequest(){ document.forms["code"].submit(); } -function check_state(state, uid, success) { - if (state == "running") { +function check_state(state, uid) { + if (state == "running" || state == "not started") { setTimeout(get_result(uid), 2000); + } else if (state == "unknown") { + request_status = "initial"; + var $notice = document.getElementById("notification"); + $notice.classList.add("alert"); + $notice.classList.add("alert-success"); + $notice.innerHTML = "Your are requesting for a wrong data"; } } @@ -24,7 +30,7 @@ function get_result(uid){ } else if(content_type.includes("application/json")) { res = JSON.parse(data); request_status = res.status; - check_state(request_status, uid, res.success); + check_state(request_status, uid); } } }); @@ -74,6 +80,7 @@ $(document).ready(function(){ success: function(data, status, xhr) { content_type = xhr.getResponseHeader("content-type"); if(content_type.includes("text/html")) { + request_status = "initial" document.open(); document.write(data); document.close(); @@ -81,7 +88,7 @@ $(document).ready(function(){ res = JSON.parse(data); var uid = res.uid; request_status = res.state; - check_state(request_status, uid, false); + check_state(request_status, uid); } } }); diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index 9df2fef..9789d25 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -101,14 +101,17 @@ lang = "{{ question.language }}"

{% if notification %} {% if question.type == "code" %} -