summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradityacp2016-08-31 15:30:52 +0530
committeradityacp2016-08-31 15:30:52 +0530
commit9dbb30e8adadf18e0e5261eda6d1aa6d62fdbb4f (patch)
treeb23b9685fb942266be5c1a748be5e49c0f596c4b
parent40fbb5d8f1d4174f7d7e2d4723e9fbfc40040dcb (diff)
parentaefc8eed3b0c18520059b4005978f1db9cf5591b (diff)
downloadonline_test-9dbb30e8adadf18e0e5261eda6d1aa6d62fdbb4f.tar.gz
online_test-9dbb30e8adadf18e0e5261eda6d1aa6d62fdbb4f.tar.bz2
online_test-9dbb30e8adadf18e0e5261eda6d1aa6d62fdbb4f.zip
rebase with latest changes
-rw-r--r--.travis.yml5
-rw-r--r--requirements.txt1
-rw-r--r--yaksh/admin.py6
-rwxr-xr-xyaksh/code_server.py118
-rw-r--r--yaksh/documentation/installation.rst50
-rw-r--r--yaksh/models.py27
-rw-r--r--yaksh/templates/user.html2
-rw-r--r--yaksh/templates/yaksh/addteacher.html4
-rw-r--r--yaksh/templates/yaksh/complete.html2
-rw-r--r--yaksh/templates/yaksh/courses.html82
-rw-r--r--yaksh/templates/yaksh/question.html23
-rw-r--r--yaksh/templates/yaksh/user_data.html10
-rw-r--r--yaksh/test_models.py (renamed from yaksh/tests.py)91
-rw-r--r--yaksh/tests/__init__.py0
-rw-r--r--yaksh/tests/test_code_server.py130
-rw-r--r--yaksh/urls.py1
-rw-r--r--yaksh/views.py90
-rw-r--r--yaksh/xmlrpc_clients.py21
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 }}">&nbsp;{{ 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>&nbsp;&nbsp;
{% elif question.type == "upload" %}
<br><button class="btn" type="submit" name="check" id="check" onClick="return validate();">Upload</button>&nbsp;&nbsp;
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