diff options
author | adityacp | 2016-08-31 15:30:52 +0530 |
---|---|---|
committer | adityacp | 2016-08-31 15:30:52 +0530 |
commit | 9dbb30e8adadf18e0e5261eda6d1aa6d62fdbb4f (patch) | |
tree | b23b9685fb942266be5c1a748be5e49c0f596c4b | |
parent | 40fbb5d8f1d4174f7d7e2d4723e9fbfc40040dcb (diff) | |
parent | aefc8eed3b0c18520059b4005978f1db9cf5591b (diff) | |
download | online_test-9dbb30e8adadf18e0e5261eda6d1aa6d62fdbb4f.tar.gz online_test-9dbb30e8adadf18e0e5261eda6d1aa6d62fdbb4f.tar.bz2 online_test-9dbb30e8adadf18e0e5261eda6d1aa6d62fdbb4f.zip |
rebase with latest changes
-rw-r--r-- | .travis.yml | 5 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | yaksh/admin.py | 6 | ||||
-rwxr-xr-x | yaksh/code_server.py | 118 | ||||
-rw-r--r-- | yaksh/documentation/installation.rst | 50 | ||||
-rw-r--r-- | yaksh/models.py | 27 | ||||
-rw-r--r-- | yaksh/templates/user.html | 2 | ||||
-rw-r--r-- | yaksh/templates/yaksh/addteacher.html | 4 | ||||
-rw-r--r-- | yaksh/templates/yaksh/complete.html | 2 | ||||
-rw-r--r-- | yaksh/templates/yaksh/courses.html | 82 | ||||
-rw-r--r-- | yaksh/templates/yaksh/question.html | 23 | ||||
-rw-r--r-- | yaksh/templates/yaksh/user_data.html | 10 | ||||
-rw-r--r-- | yaksh/test_models.py (renamed from yaksh/tests.py) | 91 | ||||
-rw-r--r-- | yaksh/tests/__init__.py | 0 | ||||
-rw-r--r-- | yaksh/tests/test_code_server.py | 130 | ||||
-rw-r--r-- | yaksh/urls.py | 1 | ||||
-rw-r--r-- | yaksh/views.py | 90 | ||||
-rw-r--r-- | yaksh/xmlrpc_clients.py | 21 |
18 files changed, 507 insertions, 156 deletions
diff --git a/.travis.yml b/.travis.yml index d362005..8ad6c5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,8 +9,9 @@ env: # command to install dependencies install: + - pip install tornado - pip install git+https://github.com/FOSSEE/online_test.git#egg=yaksh-0.1 - - pip install -q Django==$DJANGO --use-mirrors + - pip install -q Django==$DJANGO - pip install -q pytz==2016.4 - pip install -q python-social-auth==0.2.19 @@ -20,4 +21,4 @@ before_install: # command to run tests script: - - python manage.py test yaksh + - python manage.py test -v 2 yaksh diff --git a/requirements.txt b/requirements.txt index 5438d8a..bea0017 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ mysql-python==1.2.5 django-taggit==0.18.1 pytz==2016.4 python-social-auth==0.2.19 +tornado diff --git a/yaksh/admin.py b/yaksh/admin.py index ddd5e62..b5c8933 100644 --- a/yaksh/admin.py +++ b/yaksh/admin.py @@ -1,9 +1,11 @@ -from yaksh.models import Question, Quiz -from yaksh.models import TestCase, StandardTestCase, StdioBasedTestCase +from yaksh.models import Question, Quiz, QuestionPaper +from yaksh.models import TestCase, StandardTestCase, StdioBasedTestCase, Course from django.contrib import admin admin.site.register(Question) admin.site.register(TestCase) admin.site.register(StandardTestCase) admin.site.register(StdioBasedTestCase) +admin.site.register(Course) admin.site.register(Quiz) +admin.site.register(QuestionPaper) diff --git a/yaksh/code_server.py b/yaksh/code_server.py index 2d8567e..e19e9c8 100755 --- a/yaksh/code_server.py +++ b/yaksh/code_server.py @@ -1,9 +1,11 @@ #!/usr/bin/env python -"""This server runs an XMLRPC server 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) 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:: $ sudo ./code_server.py # Runs servers based on settings.py:SERVER_PORTS one server per port given. @@ -17,18 +19,32 @@ 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. + """ -import sys + +# Standard library imports from SimpleXMLRPCServer import SimpleXMLRPCServer -import pwd +import json +from multiprocessing import Process, Queue import os -import stat from os.path import isdir, dirname, abspath, join, isfile +import pwd +import re import signal -from multiprocessing import Process, Queue +import stat import subprocess -import re -import json +import sys + +try: + from urllib import unquote +except ImportError: + # The above import will not work on Python-3.x. + from urllib.parse import unquote + +# Library imports +from tornado.ioloop import IOLoop +from tornado.web import Application, RequestHandler + # Local imports. from settings import SERVER_PORTS, SERVER_POOL_PORT from language_registry import create_evaluator_instance, unpack_json @@ -62,7 +78,7 @@ class CodeServer(object): """Calls relevant EvaluateCode class based on language to check the answer code """ - code_evaluator = create_evaluator_instance(language, + code_evaluator = create_evaluator_instance(language, test_case_type, json_data, in_dir @@ -104,15 +120,30 @@ class ServerPool(object): """ self.my_port = pool_port self.ports = ports - queue = Queue(maxsize=len(ports)) + queue = Queue(maxsize=len(self.ports)) self.queue = queue servers = [] - for port in ports: + processes = [] + for port in self.ports: server = CodeServer(port, queue) servers.append(server) p = Process(target=server.run) - p.start() + processes.append(p) self.servers = servers + self.processes = processes + self.app = self._make_app() + + def _make_app(self): + app = Application([ + (r"/.*", MainHandler, dict(server=self)), + ]) + app.listen(self.my_port) + return app + + def _start_code_servers(self): + for proc in self.processes: + if proc.pid is None: + proc.start() # Public Protocol ########## @@ -120,36 +151,63 @@ class ServerPool(object): """Get available server port from ones in the pool. This will block till it gets an available server. """ - q = self.queue - was_waiting = True if q.empty() else False - port = q.get() - if was_waiting: - print '*'*80 - print "No available servers, was waiting but got server \ - later at %d." % port - print '*'*80 - sys.stdout.flush() - return port + 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) def run(self): """Run server which returns an available server port where code can be executed. """ - server = SimpleXMLRPCServer(("0.0.0.0", self.my_port)) + # We start the code servers here to ensure they are run as nobody. + self._start_code_servers() + IOLoop.current().start() + + def stop(self): + """Stop all the code server processes. + """ + for proc in self.processes: + proc.terminate() + IOLoop.current().stop() + + +class MainHandler(RequestHandler): + def initialize(self, server): self.server = server - server.register_instance(self) - server.serve_forever() + + 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 + self.write(result) ############################################################################### def main(args=None): - run_as_nobody() if args: - ports = [int(x) for x in args[1:]] + ports = [int(x) for x in args] else: ports = SERVER_PORTS server_pool = ServerPool(ports=ports, pool_port=SERVER_POOL_PORT) + # This is done *after* the server pool is created because when the tornado + # app calls listen(), it cannot be nobody. + run_as_nobody() + server_pool.run() if __name__ == '__main__': diff --git a/yaksh/documentation/installation.rst b/yaksh/documentation/installation.rst index 4acee61..51efea7 100644 --- a/yaksh/documentation/installation.rst +++ b/yaksh/documentation/installation.rst @@ -64,4 +64,52 @@ This starts the code server **And entering the following admin credentials** * Username: admin - * Password: admin
\ No newline at end of file + * Password: admin + +Running The Code Server +----------------------- + +**Local Instance**: + +In a new terminal run the command:: + + sudo python /path/to/code_server.py + +Keep this instance running in the background + +**Using Docker**: + +1. Install docker + +2. Create a Docker Image using the Docker file: + + * Go to the directory where the project is located:: + + cd /path/to/online_test + + * Build a docker image using the Dockerfile:: + + sudo docker build --tag=yaksh_code_server:v1 . + +3. Start a Docker container:: + + docker run -d -p 8001:8001 -p 53579:53579 -v /path/to/online_test/yaksh/output:/src/yaksh/output yaksh_code_server:v1 + +**Note**: + * The default ports on which the code server runs and the pool port on which the former ports are available is specified in online_test/yaksh/settings.py. The code server also supports multiple ports + + * The server port is 8001 by default, this can be changed in the settings:: + + SERVER_PORTS = 8001 + + * Multiple ports can be specified as:: + + SERVER_PORTS = [8001, 8002, 8003, 8004, 8005] # Or use range(8001, 8040) for larger number of ports + + * The default pool port is 53579 by default, this can be changed in the settings:: + + SERVER_POOL_PORT = 53579 + + * The docker command to start a docker container when using multiple ports is:: + + docker run -d -p 8001-8039:8001-8039 -p 53579:53579 yaksh_code_server:v1 diff --git a/yaksh/models.py b/yaksh/models.py index bdd3875..d2bffbe 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -570,7 +570,9 @@ class QuestionPaper(models.Model): if self.quiz.has_prerequisite(): prerequisite = self._get_prequisite_paper() return prerequisite._is_questionpaper_passed(user) - + + def __unicode__(self): + return "Question Paper for " + self.quiz.description ############################################################################### class QuestionSet(models.Model): @@ -784,21 +786,28 @@ class AnswerPaper(models.Model): Adds the completed question to the list of answered questions and returns the next question. """ + next_question = self.next_question(question_id) self.questions_answered.add(question_id) self.questions_unanswered.remove(question_id) + if next_question.id == int(question_id): + return None + return next_question - return self.current_question() - - def skip(self, question_id): + def next_question(self, question_id): """ Skips the current question and returns the next sequentially available question. """ - questions = self.questions_unanswered.all() - question_cycle = cycle(questions) - for question in question_cycle: - if question.id==int(question_id): - return question_cycle.next() + unanswered_questions = self.questions_unanswered.all() + questions = list(unanswered_questions.values_list('id', flat=True)) + if len(questions) == 0: + return None + try: + index = questions.index(int(question_id)) + next_id = questions[index+1] + except (ValueError, IndexError): + next_id = questions[0] + return unanswered_questions.get(id=next_id) def time_left(self): """Return the time remaining for the user in seconds.""" diff --git a/yaksh/templates/user.html b/yaksh/templates/user.html index 4074656..009dd2f 100644 --- a/yaksh/templates/user.html +++ b/yaksh/templates/user.html @@ -34,7 +34,7 @@ <li><a href="{{ URL_ROOT }}/exam/changepassword">Change Password</a></li> </ul> <ul style="float:right;"> - <li><strong><a style='cursor:pointer' onClick='location.replace("{{URL_ROOT}}/exam/complete/");'>Log out</a></strong></li> + <li><strong><a style='cursor:pointer' onClick='location.replace("{{URL_ROOT}}/exam/complete/");' id='logout'>Log out</a></strong></li> </ul> </div> </div> diff --git a/yaksh/templates/yaksh/addteacher.html b/yaksh/templates/yaksh/addteacher.html index 7e04f71..6722a52 100644 --- a/yaksh/templates/yaksh/addteacher.html +++ b/yaksh/templates/yaksh/addteacher.html @@ -18,6 +18,7 @@ <button class="btn" type="button" name="button" onClick='location.replace("{{URL_ROOT}}/exam/manage/courses");'>Cancel</button> </center></form> </div> <br><br> + <form action="{{ URL_ROOT }}/exam/manage/addteacher/{{ course.id }}/" method="post"> {% csrf_token %} {% if success == True %} @@ -56,8 +57,7 @@ </form> {% if status == True %} <div class="row"> - <div class="span6 offset4 wrap"> - <center><b><u>Teacher(s) Added</u></b></center><br> + <div class="span6 offset4 wrap"> {% if teachers_added %} {% for teacher in teachers_added %} <div class="well"> diff --git a/yaksh/templates/yaksh/complete.html b/yaksh/templates/yaksh/complete.html index 07cbf3a..98adf9b 100644 --- a/yaksh/templates/yaksh/complete.html +++ b/yaksh/templates/yaksh/complete.html @@ -29,5 +29,5 @@ <center><h2> Good bye! </h2></center> <center><h4> {{message}} </h4></center> <br><center><h4>You may now close the browser.</h4></center><br> - <center><a href="{{URL_ROOT}}/exam/"> Login Again </a></center> + <center><a href="{{URL_ROOT}}/exam/" id="login_again"> Login Again </a></center> {% endblock content %} diff --git a/yaksh/templates/yaksh/courses.html b/yaksh/templates/yaksh/courses.html index 06c848c..42f49d1 100644 --- a/yaksh/templates/yaksh/courses.html +++ b/yaksh/templates/yaksh/courses.html @@ -9,15 +9,12 @@ {% endblock %} {% block manage %} -<a href="{{URL_ROOT}}/exam/manage/allotted_course/">View Allotted Courses</a><br> + {% if not courses %} - <center><h4> No new Courses added </h4></center> + <center><h4> No new Courses created </h4></center> {% else %} -<center><h3> Course(s) Added</h3></center> +<center><h3> Course(s) Created</h3></center> {% for course in courses %} - {% if user != course.creator %} - <h4> {{course.creator.get_full_name}} added you to this course</h4> - {% endif %} <div class="row show-grid"> <div class="span14"> <div class="row"> @@ -79,7 +76,76 @@ </div> <br><br> {% endfor %} - <button class="btn primary" type="button" onClick='location.replace("{{URL_ROOT}}/exam/manage/add_course");'>Add New Course</button> - <button class="btn primary" type="button" onClick='location.replace("{{URL_ROOT}}/exam/manage/addquiz");'>Add New Quiz</button> + {% endif %} + +{% if allotted_courses %} + <center><h3> Course(s) Allotted </h3></center> + + {% for course in allotted_courses %} + <div class="row show-grid"> + <div class="span14"> + <div class="row"> + <div class="span6"> + <p> + <b><u>Course</u></b> + {% if course.active %} + <span class="label success">Active</span> + {% else %} + <span class="label important">Closed</span> + {% endif %} + </p> + <a href="{{URL_ROOT}}/exam/manage/course_detail/{{course.id}}/">{{ course.name }}</a> + </br></br> + <div class="row"> + <div class="span6 wrap"> + <center><b><u> Course Creator</u></b></center> + {{course.creator}} + <center><b><u>Teacher(s) Added to {{ course }}</u></b></center> + {% if course.get_teachers %} + <div align="left"> + <form action="{{URL_ROOT}}/exam/manage/remove_teachers/{{ course.id }}/" method="post"> + {% csrf_token %} + {% for teacher in course.get_teachers %} + <div class="well"> + <div class="row"> + <div class="span3" style="width: auto;"> + <input type="checkbox" name="remove" value="{{ teacher.id }}"> {{ teacher.get_full_name }} + </div> + </div> + </div> + {% endfor %} + <button class="btn success" type="submit">Remove Selected</button> + </div> + {% else %} + <center><b>No Teacher(s) Added</b></center> + {% endif %} + </form> + </div> + </div> + </div> + <div class="span6"> + <p><b><a href="{{URL_ROOT}}/exam/manage/searchteacher/{{course.id}}/">Add Teacher</a></b></p> + </div> + <div class="span6"> + <p><b><u>Quiz(zes)</u></b></p> + {% if course.get_quizzes %} + {% for quiz in course.get_quizzes %} + <a href="{{URL_ROOT}}/exam/manage/addquiz/{{quiz.id}}/">{{ quiz.description }}</a><br> + {% endfor %} + {% else %} + <p><b>No quiz </b></p> + {% endif %} + </div> + </div> + </div> + </div> + <br><br> + {% endfor %} +{% else %} + <center><h4> No new Courses allotted </h4></center> {% endif %} +<button class="btn primary" type="button" onClick='location.replace("{{URL_ROOT}}/exam/manage/add_course");'>Add New Course</button> + {% if courses or allotted_courses %} + <button class="btn primary" type="button" onClick='location.replace("{{URL_ROOT}}/exam/manage/addquiz");'>Add New Quiz</button> +{% endif %} {% endblock %} diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index 2d52009..9a0f899 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -85,21 +85,6 @@ function call_skip(url) form.action = url form.submit(); } - {% if error_message == 'Correct Output'%} - {% if paper.questions_left %} - window.setTimeout(function() - { - {% for qid in paper.questions.all %} - location.href="{{ URL_ROOT }}/exam/{{ qid.id }}/check/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/" - {% endfor %} - }, 2000); - {% else %} - window.setTimeout(function() - { - location.href="{{ URL_ROOT }}/exam/{{ question.id }}/check/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/" - }, 2000); - {% endif %} - {% endif %} </script> {% endblock script %} @@ -167,11 +152,17 @@ function call_skip(url) <input type=hidden name="question_id" id="question_id" value={{ question.id }}></input> {% if question.type == "mcq" %} + {% if error_message %} + <p>{{ error_message }}</p> + {% endif %} {% for test_case in test_cases %} <input name="answer" type="radio" value="{{ test_case.options }}" />{{ test_case.options }} <br/> {% endfor %} {% endif %} {% if question.type == "mcc" %} + {% if error_message %} + <p>{{ error_message }}</p> + {% endif %} {% for test_case in test_cases %} <input name="answer" type="checkbox" value="{{ test_case.options }}"> {{ test_case.options }} <br> @@ -190,7 +181,7 @@ function call_skip(url) {% endif %} - {% if question.type == "mcq" or question.type == "mcc "%} + {% if question.type == "mcq" or question.type == "mcc"%} <br><button class="btn" type="submit" name="check" id="check">Submit Answer</button> {% elif question.type == "upload" %} <br><button class="btn" type="submit" name="check" id="check" onClick="return validate();">Upload</button> diff --git a/yaksh/templates/yaksh/user_data.html b/yaksh/templates/yaksh/user_data.html index 04544f9..2e7db50 100644 --- a/yaksh/templates/yaksh/user_data.html +++ b/yaksh/templates/yaksh/user_data.html @@ -56,11 +56,15 @@ User IP address: {{ paper.user_ip }} </p> <p>Student answer: {{ answers.0 }}</p> {% else %}{# non-mcq questions #} -<pre> -{% for answer in answers %}################################################################################ +{% for answer in answers %} +{% if not answer.skipped %} +<pre> +############################################################################### {{ answer.answer.strip }} # Autocheck: {{ answer.error }} -{% endfor %}</pre> +</pre> +{% endif %} +{% endfor %} {% endif %} {% with answers|last as answer %} <p><em>Marks: {{answer.marks}} </em> </p> diff --git a/yaksh/tests.py b/yaksh/test_models.py index 5b87fc1..4861fee 100644 --- a/yaksh/tests.py +++ b/yaksh/test_models.py @@ -424,7 +424,7 @@ class AnswerPaperTestCases(unittest.TestCase): self.answerpaper.save() # answers for the Answer Paper self.answer_right = Answer(question=Question.objects.get(id=1), - answer="Demo answer", + answer="Demo answer", correct=True, marks=1 ) self.answer_wrong = Answer(question=Question.objects.get(id=2), @@ -458,20 +458,85 @@ class AnswerPaperTestCases(unittest.TestCase): # Test completed_question() method of Answer Paper question = self.answerpaper.completed_question(1) self.assertEqual(self.answerpaper.questions_left(), 2) - # Test skip() method of Answer Paper + + # Test next_question() method of Answer Paper current_question = self.answerpaper.current_question() self.assertEqual(current_question.id, 2) - next_question_id = self.answerpaper.skip(current_question.id) + + # When + next_question_id = self.answerpaper.next_question(current_question.id) + + # Then self.assertTrue(next_question_id is not None) self.assertEqual(next_question_id.id, 3) + + # Given, here question is already answered + current_question_id = 1 + + # When + next_question_id = self.answerpaper.next_question(current_question_id) + + # Then + self.assertTrue(next_question_id is not None) + self.assertEqual(next_question_id.id, 2) + + # Given, wrong question id + current_question_id = 12 + + # When + next_question_id = self.answerpaper.next_question(current_question_id) + + # Then + self.assertTrue(next_question_id is not None) + self.assertEqual(next_question_id.id, 2) + + # Given, last question in the list + current_question_id = 3 + + # When + next_question_id = self.answerpaper.next_question(current_question_id) + + # Then + self.assertTrue(next_question_id is not None) + self.assertEqual(next_question_id.id, 2) + + # Test get_questions_answered() method + # When questions_answered = self.answerpaper.get_questions_answered() + + # Then self.assertEqual(questions_answered.count(), 1) self.assertSequenceEqual(questions_answered, [self.questions[0]]) + + # When questions_unanswered = self.answerpaper.get_questions_unanswered() + + # Then self.assertEqual(questions_unanswered.count(), 2) self.assertSequenceEqual(questions_unanswered, [self.questions[1], self.questions[2]]) + # Test completed_question and next_question + # When all questions are answered + current_question = self.answerpaper.completed_question(2) + + # Then + self.assertEqual(self.answerpaper.questions_left(), 1) + self.assertEqual(current_question.id, 3) + + # When + current_question = self.answerpaper.completed_question(3) + + # Then + self.assertEqual(self.answerpaper.questions_left(), 0) + self.assertTrue(current_question is None) + + # When + next_question_id = self.answerpaper.next_question(current_question_id) + + # Then + self.assertTrue(next_question_id is None) + def test_update_marks(self): """ Test update_marks method of AnswerPaper""" self.answerpaper.update_marks('inprogress') @@ -584,7 +649,7 @@ class CourseTestCases(unittest.TestCase): def test_add_teachers(self): """ Test to add teachers to a course""" self.course.add_teachers(self.student1, self.student2) - self.assertSequenceEqual(self.course.get_teachers(), + self.assertSequenceEqual(self.course.get_teachers(), [self.student1, self.student2]) def test_remove_teachers(self): @@ -616,21 +681,21 @@ class CourseTestCases(unittest.TestCase): class TestCaseTestCases(unittest.TestCase): def setUp(self): self.user = User.objects.get(pk=1) - self.question1 = Question(summary='Demo question 1', + self.question1 = Question(summary='Demo question 1', language='Python', - type='Code', + type='Code', active=True, - description='Write a function', + description='Write a function', points=1.0, - test_case_type="standardtestcase", + test_case_type="standardtestcase", user=self.user, snippet='def myfunc()' ) - self.question2 = Question(summary='Demo question 2', + self.question2 = Question(summary='Demo question 2', language='Python', - type='Code', + type='Code', active=True, - description='Write to standard output', + description='Write to standard output', points=1.0, test_case_type="stdiobasedtestcase", user=self.user, @@ -658,13 +723,13 @@ class TestCaseTestCases(unittest.TestCase): def test_assertion_testcase(self): """ Test question """ self.assertEqual(self.assertion_testcase.question, self.question1) - self.assertEqual(self.assertion_testcase.test_case, + self.assertEqual(self.assertion_testcase.test_case, 'assert myfunc(12, 13) == 15') def test_stdout_based_testcase(self): """ Test question """ self.assertEqual(self.stdout_based_testcase.question, self.question2) - self.assertEqual(self.stdout_based_testcase.expected_output, + self.assertEqual(self.stdout_based_testcase.expected_output, 'Hello World' ) diff --git a/yaksh/tests/__init__.py b/yaksh/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/yaksh/tests/__init__.py diff --git a/yaksh/tests/test_code_server.py b/yaksh/tests/test_code_server.py new file mode 100644 index 0000000..a73f073 --- /dev/null +++ b/yaksh/tests/test_code_server.py @@ -0,0 +1,130 @@ +import json +try: + from Queue import Queue +except ImportError: + from queue import Queue +from threading import Thread +import unittest +import urllib + +from yaksh.code_server import ServerPool, SERVER_POOL_PORT +from yaksh import settings +from yaksh.xmlrpc_clients import CodeServerProxy + + +class TestCodeServer(unittest.TestCase): + + @classmethod + 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) + cls.server_pool = server_pool + cls.server_thread = t = Thread(target=server_pool.run) + t.start() + + @classmethod + def tearDownClass(cls): + cls.server_pool.stop() + cls.server_thread.join() + settings.code_evaluators['python']['standardtestcase'] = \ + "python_assertion_evaluator.PythonAssertionEvaluator" + + def setUp(self): + self.code_server = CodeServerProxy() + + def test_inifinite_loop(self): + # Given + testdata = {'user_answer': 'while True: pass', + 'test_case_data': [{'test_case':'assert 1==2'}]} + + # When + result = self.code_server.run_code( + 'python', 'standardtestcase', json.dumps(testdata), '' + ) + + # Then + data = json.loads(result) + self.assertFalse(data['success']) + self.assertTrue('infinite loop' in data['error']) + + def test_correct_answer(self): + # Given + testdata = {'user_answer': 'def f(): return 1', + 'test_case_data': [{'test_case':'assert f() == 1'}]} + + # When + result = self.code_server.run_code( + 'python', 'standardtestcase', json.dumps(testdata), '' + ) + + # Then + data = json.loads(result) + self.assertTrue(data['success']) + self.assertEqual(data['error'], 'Correct answer') + + def test_wrong_answer(self): + # Given + testdata = {'user_answer': 'def f(): return 1', + 'test_case_data': [{'test_case':'assert f() == 2'}]} + + # When + result = self.code_server.run_code( + 'python', 'standardtestcase', json.dumps(testdata), '' + ) + + # Then + data = json.loads(result) + self.assertFalse(data['success']) + self.assertTrue('AssertionError' in data['error']) + + def test_multiple_simultaneous_hits(self): + # Given + results = Queue() + + def run_code(): + """Run an infinite loop.""" + testdata = {'user_answer': 'while True: pass', + 'test_case_data': [{'test_case':'assert 1==2'}]} + result = self.code_server.run_code( + 'python', 'standardtestcase', json.dumps(testdata), '' + ) + results.put(json.loads(result)) + + N = 10 + # When + import time + threads = [] + for i in range(N): + t = Thread(target=run_code) + threads.append(t) + t.start() + + for t in threads: + if t.isAlive(): + t.join() + + # Then + self.assertEqual(results.qsize(), N) + for i in range(N): + data = results.get() + self.assertFalse(data['success']) + self.assertTrue('infinite loop' in data['error']) + + def test_server_pool_status(self): + # Given + url = "http://localhost:%s/status"%SERVER_POOL_PORT + + # When + data = urllib.urlopen(url).read() + + # Then + expect = 'out of 5 are free' + self.assertTrue(expect in data) + expect = 'Load:' + self.assertTrue(expect in data) + + +if __name__ == '__main__': + unittest.main() diff --git a/yaksh/urls.py b/yaksh/urls.py index cd97dd4..d14ed1d 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -97,7 +97,6 @@ urlpatterns += [ views.reject, {'was_enrolled': True}), url(r'^manage/searchteacher/(?P<course_id>\d+)/$', views.search_teacher), url(r'^manage/addteacher/(?P<course_id>\d+)/$', views.add_teacher, name='add_teacher'), - url(r'^manage/allotted_course/$', views.allotted_courses), url(r'^manage/remove_teachers/(?P<course_id>\d+)/$', views.remove_teachers, name='remove_teacher'), url(r'^manage/download_questions/$', views.show_all_questions), url(r'^manage/upload_questions/$', views.show_all_questions), diff --git a/yaksh/views.py b/yaksh/views.py index e1ec44e..923b3c2 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -454,16 +454,22 @@ def skip(request, q_id, next_q=None, attempt_num=None, questionpaper_id=None): paper = get_object_or_404(AnswerPaper, user=request.user, attempt_number=attempt_num, question_paper=questionpaper_id) question = get_object_or_404(Question, pk=q_id) + if question in paper.questions_answered.all(): + next_q = paper.next_question(q_id) + return show_question(request, next_q, paper) + if request.method == 'POST' and question.type == 'code': user_code = request.POST.get('answer') new_answer = Answer(question=question, answer=user_code, correct=False, skipped=True) new_answer.save() paper.answers.add(new_answer) - if next_q is None: - next_q = paper.skip(q_id) if paper.skip(q_id) else question - else: + if next_q is not None: next_q = get_object_or_404(Question, pk=next_q) + if next_q not in paper.questions_unanswered.all(): + return show_question(request, question, paper) + else: + next_q = paper.next_question(q_id) return show_question(request, next_q, paper) @@ -475,7 +481,7 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None): question_paper=questionpaper_id) question = get_object_or_404(Question, pk=q_id) if question in paper.questions_answered.all(): - next_q = paper.skip(q_id) + next_q = paper.next_question(q_id) return show_question(request, next_q, paper) if request.method == 'POST': @@ -504,7 +510,9 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None): correct=False) new_answer.save() paper.answers.add(new_answer) - + if not user_answer: + msg = "Please submit a valid option or code" + return show_question(request, question, paper, msg) # 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 # safely in a separate process (the code_server.py) running as nobody. @@ -525,17 +533,8 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None): new_answer.save() return show_question(request, question, paper, result.get('error')) else: - # Display the same question if user_answer is None - if not user_answer: - msg = "Please submit a valid option or code" - return show_question(request, question, paper, msg) - elif question.type == 'code' and user_answer: - msg = "Correct Output" - paper.completed_question(question.id) - return show_question(request, question, paper, msg) - else: - next_q = paper.completed_question(question.id) - return show_question(request, next_q, paper) + next_q = paper.completed_question(question.id) + return show_question(request, next_q, paper) else: return show_question(request, question, paper) @@ -558,11 +557,13 @@ def validate_answer(user, user_answer, question, json_data=None): expected_answer = question.get_test_case(correct=True).options if user_answer.strip() == expected_answer.strip(): correct = True + result['error'] = 'Correct answer' elif question.type == 'mcc': expected_answers = [] for opt in question.get_test_cases(correct=True): expected_answers.append(opt.options) if set(user_answer) == set(expected_answers): + result['error'] = 'Correct answer' correct = True elif question.type == 'code': user_dir = get_user_dir(user) @@ -664,8 +665,10 @@ def courses(request): if not is_moderator(user): raise Http404('You are not allowed to view this page') courses = Course.objects.filter(creator=user, is_trial=False) - return my_render_to_response('yaksh/courses.html', {'courses': courses}, - context_instance=ci) + allotted_courses = Course.objects.filter(teachers=user, is_trial=False) + context = {'courses': courses, "allotted_courses": allotted_courses} + return my_render_to_response('yaksh/courses.html', context, + context_instance=ci) @login_required @@ -1164,25 +1167,24 @@ def search_teacher(request, course_id): raise Http404('You are not allowed to view this page!') context = {} - course = get_object_or_404(Course, creator=user, pk=course_id) + course = get_object_or_404(Course, pk=course_id) context['course'] = course + if user != course.creator and user not in course.teachers.all(): + raise Http404('You are not allowed to view this page!') + if request.method == 'POST': u_name = request.POST.get('uname') - if len(u_name) == 0: - return my_render_to_response('yaksh/addteacher.html', context, - context_instance=ci) - else: + if not len(u_name) == 0: teachers = User.objects.filter(Q(username__icontains=u_name)| Q(first_name__icontains=u_name)|Q(last_name__icontains=u_name)| - Q(email__icontains=u_name)).exclude(Q(id=user.id)|Q(is_superuser=1)) + Q(email__icontains=u_name)).exclude(Q(id=user.id)|Q(is_superuser=1)| + Q(id=course.creator.id)) context['success'] = True context['teachers'] = teachers - return my_render_to_response('yaksh/addteacher.html', context, - context_instance=ci) - else: - return my_render_to_response('yaksh/addteacher.html', context, - context_instance=ci) + + return my_render_to_response('yaksh/addteacher.html', context, + context_instance=ci) @login_required @@ -1196,8 +1198,11 @@ def add_teacher(request, course_id): raise Http404('You are not allowed to view this page!') context = {} - course = get_object_or_404(Course, creator=user, pk=course_id) - context['course'] = course + course = get_object_or_404(Course, pk=course_id) + if user == course.creator or user in course.teachers.all(): + context['course'] = course + else: + raise Http404('You are not allowed to view this page!') if request.method == 'POST': teacher_ids = request.POST.getlist('check') @@ -1206,25 +1211,10 @@ def add_teacher(request, course_id): course.add_teachers(*teachers) context['status'] = True context['teachers_added'] = teachers - return my_render_to_response('yaksh/addteacher.html', context, + + return my_render_to_response('yaksh/addteacher.html', context, context_instance=ci) - else: - return my_render_to_response('yaksh/addteacher.html', context, - context_instance=ci) - - -@login_required -def allotted_courses(request): - """ show courses allotted to a user """ - user = request.user - ci = RequestContext(request) - if not is_moderator(user): - raise Http404('You are not allowed to view this page!') - - courses = Course.objects.filter(teachers=user) - return my_render_to_response('yaksh/courses.html', {'courses': courses}, - context_instance=ci) @login_required @@ -1232,10 +1222,10 @@ def remove_teachers(request, course_id): """ remove user from a course """ user = request.user - if not is_moderator(user): + course = get_object_or_404(Course, pk=course_id) + if not is_moderator(user) and (user != course.creator and user not in course.teachers.all()): raise Http404('You are not allowed to view this page!') - course = get_object_or_404(Course, creator=user, pk=course_id) if request.method == "POST": teacher_ids = request.POST.getlist('remove') teachers = User.objects.filter(id__in=teacher_ids) diff --git a/yaksh/xmlrpc_clients.py b/yaksh/xmlrpc_clients.py index 7124550..6bfe0d6 100644 --- a/yaksh/xmlrpc_clients.py +++ b/yaksh/xmlrpc_clients.py @@ -3,6 +3,7 @@ import time import random import socket import json +import urllib from settings import SERVER_PORTS, SERVER_POOL_PORT @@ -21,7 +22,7 @@ class CodeServerProxy(object): """ def __init__(self): pool_url = 'http://localhost:%d' % (SERVER_POOL_PORT) - self.pool_server = ServerProxy(pool_url) + self.pool_url = pool_url def run_code(self, language, test_case_type, json_data, user_dir): """Tests given code (`answer`) with the `test_code` supplied. If the @@ -34,7 +35,7 @@ class CodeServerProxy(object): ---------- json_data contains; user_answer : str - The user's answer for the question. + The user's answer for the question. test_code : str The test code to check the user code with. language : str @@ -57,21 +58,7 @@ class CodeServerProxy(object): return result def _get_server(self): - # Get a suitable server from our pool of servers. This may block. We - # try about 60 times, essentially waiting at most for about 30 seconds. - done, count = False, 60 - - while not done and count > 0: - try: - port = self.pool_server.get_server_port() - except socket.error: - # Wait a while try again. - time.sleep(random.random()) - count -= 1 - else: - done = True - if not done: - raise ConnectionError("Couldn't connect to a server!") + port = json.loads(urllib.urlopen(self.pool_url).read()) proxy = ServerProxy('http://localhost:%d' % port) return proxy |