From 861d2cc5e36835f60bace61a919e73b4bd27274b Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 04:02:19 +0530 Subject: Moved all the apps to testapp folder --- README.txt | 190 ----------- __init__.py | 0 apache/django.wsgi | 18 -- code_server.py | 360 --------------------- docs/sample.args | 2 - docs/sample.sh | 2 - docs/sample_questions.py | 59 ---- docs/sample_questions.xml | 43 --- exam/__init__.py | 0 exam/admin.py | 5 - exam/forms.py | 95 ------ exam/management/__init__.py | 0 exam/management/commands/__init__.py | 0 exam/management/commands/dump_user_data.py | 98 ------ exam/management/commands/load_exam.py | 55 ---- exam/management/commands/load_questions_xml.py | 73 ----- exam/management/commands/results2csv.py | 69 ---- exam/migrations/0001_initial.py | 193 ----------- exam/migrations/__init__.py | 0 exam/models.py | 221 ------------- exam/tests.py | 16 - exam/urls.py | 16 - exam/views.py | 351 -------------------- exam/xmlrpc_clients.py | 78 ----- manage.py | 14 - output/README.txt | 4 - settings.py | 188 ----------- static/exam/css/base.css | 26 -- templates/404.html | 5 - templates/500.html | 7 - templates/base.html | 25 -- templates/exam/complete.html | 12 - templates/exam/grade_user.html | 83 ----- templates/exam/intro.html | 53 --- templates/exam/login.html | 20 -- templates/exam/monitor.html | 67 ---- templates/exam/question.html | 91 ------ templates/exam/quit.html | 14 - templates/exam/register.html | 17 - templates/exam/user_data.html | 84 ----- test_server.py | 83 ----- testapp/README.txt | 190 +++++++++++ testapp/__init__.py | 0 testapp/apache/django.wsgi | 18 ++ testapp/code_server.py | 360 +++++++++++++++++++++ testapp/docs/sample.args | 2 + testapp/docs/sample.sh | 2 + testapp/docs/sample_questions.py | 59 ++++ testapp/docs/sample_questions.xml | 43 +++ testapp/exam/__init__.py | 0 testapp/exam/admin.py | 5 + testapp/exam/forms.py | 95 ++++++ testapp/exam/management/__init__.py | 0 testapp/exam/management/commands/__init__.py | 0 testapp/exam/management/commands/dump_user_data.py | 98 ++++++ testapp/exam/management/commands/load_exam.py | 55 ++++ .../exam/management/commands/load_questions_xml.py | 73 +++++ testapp/exam/management/commands/results2csv.py | 69 ++++ testapp/exam/migrations/0001_initial.py | 193 +++++++++++ testapp/exam/migrations/__init__.py | 0 testapp/exam/models.py | 221 +++++++++++++ testapp/exam/tests.py | 16 + testapp/exam/urls.py | 16 + testapp/exam/views.py | 351 ++++++++++++++++++++ testapp/exam/xmlrpc_clients.py | 78 +++++ testapp/manage.py | 14 + testapp/output/README.txt | 4 + testapp/settings.py | 188 +++++++++++ testapp/static/exam/css/base.css | 26 ++ testapp/templates/404.html | 5 + testapp/templates/500.html | 7 + testapp/templates/base.html | 25 ++ testapp/templates/exam/complete.html | 12 + testapp/templates/exam/grade_user.html | 83 +++++ testapp/templates/exam/intro.html | 53 +++ testapp/templates/exam/login.html | 20 ++ testapp/templates/exam/monitor.html | 67 ++++ testapp/templates/exam/question.html | 91 ++++++ testapp/templates/exam/quit.html | 14 + testapp/templates/exam/register.html | 17 + testapp/templates/exam/user_data.html | 84 +++++ testapp/test_server.py | 83 +++++ testapp/urls.py | 23 ++ urls.py | 23 -- 84 files changed, 2760 insertions(+), 2760 deletions(-) delete mode 100644 README.txt delete mode 100644 __init__.py delete mode 100644 apache/django.wsgi delete mode 100755 code_server.py delete mode 100644 docs/sample.args delete mode 100755 docs/sample.sh delete mode 100644 docs/sample_questions.py delete mode 100644 docs/sample_questions.xml delete mode 100644 exam/__init__.py delete mode 100644 exam/admin.py delete mode 100644 exam/forms.py delete mode 100644 exam/management/__init__.py delete mode 100644 exam/management/commands/__init__.py delete mode 100644 exam/management/commands/dump_user_data.py delete mode 100644 exam/management/commands/load_exam.py delete mode 100644 exam/management/commands/load_questions_xml.py delete mode 100644 exam/management/commands/results2csv.py delete mode 100644 exam/migrations/0001_initial.py delete mode 100644 exam/migrations/__init__.py delete mode 100644 exam/models.py delete mode 100644 exam/tests.py delete mode 100644 exam/urls.py delete mode 100644 exam/views.py delete mode 100644 exam/xmlrpc_clients.py delete mode 100755 manage.py delete mode 100644 output/README.txt delete mode 100644 settings.py delete mode 100644 static/exam/css/base.css delete mode 100644 templates/404.html delete mode 100644 templates/500.html delete mode 100644 templates/base.html delete mode 100644 templates/exam/complete.html delete mode 100644 templates/exam/grade_user.html delete mode 100644 templates/exam/intro.html delete mode 100644 templates/exam/login.html delete mode 100644 templates/exam/monitor.html delete mode 100644 templates/exam/question.html delete mode 100644 templates/exam/quit.html delete mode 100644 templates/exam/register.html delete mode 100644 templates/exam/user_data.html delete mode 100644 test_server.py create mode 100644 testapp/README.txt create mode 100644 testapp/__init__.py create mode 100644 testapp/apache/django.wsgi create mode 100755 testapp/code_server.py create mode 100644 testapp/docs/sample.args create mode 100755 testapp/docs/sample.sh create mode 100644 testapp/docs/sample_questions.py create mode 100644 testapp/docs/sample_questions.xml create mode 100644 testapp/exam/__init__.py create mode 100644 testapp/exam/admin.py create mode 100644 testapp/exam/forms.py create mode 100644 testapp/exam/management/__init__.py create mode 100644 testapp/exam/management/commands/__init__.py create mode 100644 testapp/exam/management/commands/dump_user_data.py create mode 100644 testapp/exam/management/commands/load_exam.py create mode 100644 testapp/exam/management/commands/load_questions_xml.py create mode 100644 testapp/exam/management/commands/results2csv.py create mode 100644 testapp/exam/migrations/0001_initial.py create mode 100644 testapp/exam/migrations/__init__.py create mode 100644 testapp/exam/models.py create mode 100644 testapp/exam/tests.py create mode 100644 testapp/exam/urls.py create mode 100644 testapp/exam/views.py create mode 100644 testapp/exam/xmlrpc_clients.py create mode 100755 testapp/manage.py create mode 100644 testapp/output/README.txt create mode 100644 testapp/settings.py create mode 100644 testapp/static/exam/css/base.css create mode 100644 testapp/templates/404.html create mode 100644 testapp/templates/500.html create mode 100644 testapp/templates/base.html create mode 100644 testapp/templates/exam/complete.html create mode 100644 testapp/templates/exam/grade_user.html create mode 100644 testapp/templates/exam/intro.html create mode 100644 testapp/templates/exam/login.html create mode 100644 testapp/templates/exam/monitor.html create mode 100644 testapp/templates/exam/question.html create mode 100644 testapp/templates/exam/quit.html create mode 100644 testapp/templates/exam/register.html create mode 100644 testapp/templates/exam/user_data.html create mode 100644 testapp/test_server.py create mode 100644 testapp/urls.py delete mode 100644 urls.py diff --git a/README.txt b/README.txt deleted file mode 100644 index a265675..0000000 --- a/README.txt +++ /dev/null @@ -1,190 +0,0 @@ -Introduction -============ - -This app provides an "exam" app that lets users take an online -programming quiz. Currently only Python and simple Bash scripts can be -tested. At FOSSEE, Nishanth had implemented a nice django based app to -test for multiple-choice questions. However, I was inspired by a -programming contest that I saw at PyCon APAC 2011. Chris Boesch, who -administered the contest, used a nice web application that he had built -on top of GAE that basically checked your Python code, live. This made -it fun and interesting. Their application can be seen at -http://singpath.com - -I wanted an implementation that was not tied to GAE and decided to write -one myself and the result is the "exam" app. The idea being that I can -use this to test students programming skills and not have to worry about -grading their answers myself and I can do so on my machines. - -You can define fairly complicated programming problems and have users -solve the problem and the solution is checked immediately. The system -supports pretty much arbitrary Python and uses "test cases" to test the -implementations of the students. It also supports simple bash scripts --- see the sample questions in "docs/". In addition it supports simple -multiple choice questions. Since it runs on your Python, you could -technically test any Python based library. It is distributed under the -BSD license. - -It can use a lot more work but the basics work and the app scales to -over 500+ simultaneous users. :) - -Dependencies -============= - -Before you install/deploy, make sure you have the following installed: - - - Django 1.3 or above. - - South (tested with 0.7.3). - -That and a running Python is pretty much all you need. Of course, for -serious deployment you are going to need Apache or some other decent -webserver. - - -Installation and Deployment -============================= - -To install/deploy this app follow the steps below: - - 1. Clone this repository and cd to the cloned repo. - - 2. Run:: - - $ python manage.py syncdb - [ enter password etc.] - - $ python manage.py migrate exam - - 3. Add questions by editing the "docs/sample_questions.py" or any other - file in the same format and then run the following:: - - $ python manage.py load_exam docs/sample_questions.py - - Note that you can supply multiple Python files as arguments and all of - those will be added to the database. - - 4. First run the python server provided. This ensures that the code is - executed in a safe environment. Do this like so:: - - $ sudo python code_server.py - - Put this in the background once it has started since this will not - return back the prompt. It is important that the server be running - *before* students start attempting the exam. Using sudo is - necessary since the server is run as the user "nobody". This runs - on the ports configured in the settings.py file in the variable - "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. - This is the maximum time allowed to execute the submitted code. - Note that this will likely spawn multiple processes as "nobody" - depending on the number of server ports specified. - - 5. Now, run:: - - $ python manage.py runserver : - - For deployment use Apache or a real webserver, see below for more - information. - - 6. Go to http://deserved_host_or_ip:desired_port/admin - - 7. Login with your credentials and look at the questions and modify if - needed. Create a new Quiz, set the date and duration or - activate/deactivate the quiz. - - 8. Now ask users to login at: - - http://host:port/exam - - And you should be all set. - - 9. Note that the directory "output" will contain directories, one for each - user. Users can potentially write output into these that can be used - for checking later. - - 10. As admin user you can visit http://host/exam/monitor to view - results and user data interactively. You could also "grade" the - papers manually if needed. - - 11. You may dump the results and user data using the results2csv and - dump_user_data commands. - -WARNING: django is running in debug mode for this currently, CHANGE it -during deployment. To do this, edit settings.py and set DEBUG to False. -Also look at other settings and change them suitably. - -The file docs/sample_questions.py is a template that you can use for your -own questions. - -Additional commands available -============================== - -We provide several convenient commands for you to use: - - - load_exam : load questions and a quiz from a python file. See - docs/sample_questions.py - - - load_questions_xml : load questions from XML file, see - docs/sample_questions.xml use of this is deprecated in favor of - load_exam. - - - results2csv : Dump the quiz results into a CSV file for further - processing. - - - dump_user_data : Dump out relevalt user data for either all users or - specified users. - -For more information on these do this:: - - $ ./manage.py help [command] - -where [command] is one of the above. - -Deploying via Apache -===================== - -For any serious deployment, you will need to deploy the app using a real -webserver like Apache. The ``apache/django.wsgi`` script should make it -easy to deploy this using mod_wsgi. You will need to add a line of the -form: - - WSGIScriptAlias / "/var/www/online_test/apache/django.wsgi" - -to your apache.conf. For more details see the Django docs here: - -https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ - - -Sometimes you might be in the situation where you are not hosted as -"host.org/exam/" but as "host.org/foo/exam/" for whatever reason. In -this case edit "settings.py" and set the "URL_ROOT" to the root you -have to serve at. In the above example for "host.org/foo/exam" set -URL_ROOT='/foo'. - -License -======= - -This is distributed under the terms of the BSD license. Copyright -information is at the bottom of this file. - -Authors -======= - -Main author: Prabhu Ramachandran - -I gratefully acknowledge help from the following: - - - Nishanth Amuluru originally from FOSSEE who wrote bulk of the - login/registration code. He wrote an initial first cut of a quiz app - which supported only simple questions which provided motivation for - this app. The current codebase does not share too much from his - implementation although there are plenty of similarities. - - - Harish Badrinath (FOSSEE) -- who provided a first cut of the bash - related scripts. - - - Srikant Patnaik and Thomas Stephen Lee, who helped deploy and test - the code. - - -Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) - diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apache/django.wsgi b/apache/django.wsgi deleted file mode 100644 index ef88526..0000000 --- a/apache/django.wsgi +++ /dev/null @@ -1,18 +0,0 @@ -import os -from os.path import dirname, abspath -import sys - -# This file is inside online_test/apache/django.wsgi -# pth should be online_test -pth = abspath(dirname(dirname(__file__))) -if pth not in sys.path: - sys.path.append(pth) -# Now add the parent of online_test also. -pth = dirname(pth) -if pth not in sys.path: - sys.path.append(pth) - -os.environ['DJANGO_SETTINGS_MODULE'] = 'online_test.settings' - -import django.core.handlers.wsgi -application = django.core.handlers.wsgi.WSGIHandler() diff --git a/code_server.py b/code_server.py deleted file mode 100755 index 1276c76..0000000 --- a/code_server.py +++ /dev/null @@ -1,360 +0,0 @@ -#!/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:: - - $ 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. -""" -import sys -import traceback -from SimpleXMLRPCServer import SimpleXMLRPCServer -import pwd -import os -import stat -from os.path import isdir, dirname, abspath, join, isfile -import signal -from multiprocessing import Process, Queue -import subprocess - -# Local imports. -from settings import SERVER_PORTS, SERVER_TIMEOUT, SERVER_POOL_PORT - -MY_DIR = abspath(dirname(__file__)) - -def run_as_nobody(): - """Runs the current process as nobody.""" - # Set the effective uid and to that of nobody. - nobody = pwd.getpwnam('nobody') - os.setegid(nobody.pw_gid) - os.seteuid(nobody.pw_uid) - - -# Raised when the code times-out. -# c.f. http://pguides.net/python/timeout-a-function -class TimeoutException(Exception): - pass - -def timeout_handler(signum, frame): - """A handler for the ALARM signal.""" - raise TimeoutException('Code took too long to run.') - - -################################################################################ -# `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 __init__(self, port, queue): - self.port = port - self.queue = queue - msg = 'Code took more than %s seconds to run. You probably '\ - 'have an infinite loop in your code.'%SERVER_TIMEOUT - self.timeout_msg = msg - - def run_python_code(self, answer, test_code, in_dir=None): - """Tests given Python function (`answer`) with the `test_code` supplied. - If the optional `in_dir` keyword argument is supplied it changes the - directory to that directory (it does not change it back to the original when - done). This function also timesout when the function takes more than - SERVER_TIMEOUT seconds to run to prevent runaway code. - - Returns - ------- - - A tuple: (success, error message). - - """ - if in_dir is not None and isdir(in_dir): - os.chdir(in_dir) - - # Add a new signal handler for the execution of this code. - old_handler = signal.signal(signal.SIGALRM, timeout_handler) - signal.alarm(SERVER_TIMEOUT) - - success = False - tb = None - try: - submitted = compile(answer, '', mode='exec') - g = {} - exec submitted in g - _tests = compile(test_code, '', mode='exec') - exec _tests in g - except TimeoutException: - err = self.timeout_msg - except AssertionError: - type, value, tb = sys.exc_info() - info = traceback.extract_tb(tb) - fname, lineno, func, text = info[-1] - text = str(test_code).splitlines()[lineno-1] - err = "{0} {1} in: {2}".format(type.__name__, str(value), text) - except: - type, value = sys.exc_info()[:2] - err = "Error: {0}".format(repr(value)) - else: - success = True - err = 'Correct answer' - finally: - del tb - # Set back any original signal handler. - signal.signal(signal.SIGALRM, old_handler) - - # Cancel the signal if any, see signal.alarm documentation. - signal.alarm(0) - - # Put us back into the server pool queue since we are free now. - self.queue.put(self.port) - - return success, err - - def run_bash_code(self, answer, test_code, in_dir=None): - """Tests given Bash code (`answer`) with the `test_code` supplied. - - The testcode should typically contain two lines, the first is a path to - the reference script we are to compare against. The second is a path - to the arguments to be supplied to the reference and submitted script. - The output of these will be compared for correctness. - - If the path's start with a "/" then we assume they are absolute paths. - If not, we assume they are relative paths w.r.t. the location of this - code_server script. - - If the optional `in_dir` keyword argument is supplied it changes the - directory to that directory (it does not change it back to the original when - done). - - Returns - ------- - - A tuple: (success, error message). - - """ - if in_dir is not None and isdir(in_dir): - os.chdir(in_dir) - - def _set_exec(fname): - os.chmod(fname, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR - |stat.S_IRGRP|stat.S_IWGRP|stat.S_IXGRP - |stat.S_IROTH|stat.S_IWOTH|stat.S_IXOTH) - submit_f = open('submit.sh', 'w') - submit_f.write(answer.lstrip()); submit_f.close() - submit_path = abspath(submit_f.name) - _set_exec(submit_path) - - ref_path, test_case_path = test_code.strip().splitlines() - if not ref_path.startswith('/'): - ref_path = join(MY_DIR, ref_path) - if not test_case_path.startswith('/'): - test_case_path = join(MY_DIR, test_case_path) - - # Add a new signal handler for the execution of this code. - old_handler = signal.signal(signal.SIGALRM, timeout_handler) - signal.alarm(SERVER_TIMEOUT) - - # Do whatever testing needed. - success = False - try: - success, err = self.check_bash_script(ref_path, submit_path, test_case_path) - except TimeoutException: - err = self.timeout_msg - except: - type, value = sys.exc_info()[:2] - err = "Error: {0}".format(repr(value)) - finally: - # Set back any original signal handler. - signal.signal(signal.SIGALRM, old_handler) - - # Delete the created file. - os.remove(submit_path) - - # Cancel the signal if any, see signal.alarm documentation. - signal.alarm(0) - - # Put us back into the server pool queue since we are free now. - self.queue.put(self.port) - - return success, err - - def _run_command(self, cmd_args, *args, **kw): - """Run a command in a subprocess while blocking, the process is killed - if it takes more than 2 seconds to run. Return the Popen object, the - stdout and stderr. - """ - try: - proc = subprocess.Popen(cmd_args, *args, **kw) - stdout, stderr = proc.communicate() - except TimeoutException: - # Runaway code, so kill it. - proc.kill() - # Re-raise exception. - raise - return proc, stdout, stderr - - def check_bash_script(self, ref_script_path, submit_script_path, - test_case_path=None): - """ Function validates student script using instructor script as - reference. Test cases can optionally be provided. The first argument - ref_script_path, is the path to instructor script, it is assumed to - have executable permission. The second argument submit_script_path, is - the path to the student script, it is assumed to have executable - permission. The Third optional argument is the path to test the - scripts. Each line in this file is a test case and each test case is - passed to the script as standard arguments. - - Returns - -------- - - returns (True, "Correct answer") : If the student script passes all test - cases/have same output, when compared to the instructor script - - returns (False, error_msg): If - the student script fails a single test/have dissimilar output, when - compared to the instructor script. - - Returns (False, error_msg): If mandatory arguments are not files or if the - required permissions are not given to the file(s). - - """ - if not isfile(ref_script_path): - return False, "No file at %s"%ref_script_path - if not isfile(submit_script_path): - return False, 'No file at %s'%submit_script_path - if not os.access(ref_script_path, os.X_OK): - return False, 'Script %s is not executable'%ref_script_path - if not os.access(submit_script_path, os.X_OK): - return False, 'Script %s is not executable'%submit_script_path - - if test_case_path is None: - ret = self._run_command(ref_script_path, stdin=None, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - proc, inst_stdout, inst_stderr = ret - ret = self._run_command(submit_script_path, stdin=None, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - proc, stdnt_stdout, stdnt_stderr = ret - if inst_stdout == stdnt_stdout: - return True, 'Correct answer' - else: - err = "Error: expected %s, got %s"%(inst_stderr, stdnt_stderr) - return False, err - else: - if not isfile(test_case_path): - return False, "No test case at %s"%test_case_path - if not os.access(ref_script_path, os.R_OK): - return False, "Test script %s, not readable"%test_case_path - valid_answer = True # We initially make it one, so that we can stop - # once a test case fails - loop_count = 0 # Loop count has to be greater than or equal to one. - # Useful for caching things like empty test files,etc. - test_cases = open(test_case_path).readlines() - num_lines = len(test_cases) - for test_case in test_cases: - loop_count += 1 - if valid_answer: - args = [ ref_script_path ] + [x for x in test_case.split()] - ret = self._run_command(args, stdin=None, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - proc, inst_stdout, inst_stderr = ret - args = [ submit_script_path ] + [x for x in test_case.split()] - ret = self._run_command(args, stdin=None, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - proc, stdnt_stdout, stdnt_stderr = ret - valid_answer = inst_stdout == stdnt_stdout - if valid_answer and (num_lines == loop_count): - return True, "Correct answer" - else: - err = "Error: expected %s, got %s"%(inst_stdout+inst_stderr, - stdnt_stdout+stdnt_stderr) - return False, err - - - def run(self): - """Run XMLRPC server, serving our methods. - """ - server = SimpleXMLRPCServer(("localhost", self.port)) - self.server = server - server.register_instance(self) - self.queue.put(self.port) - server.serve_forever() - - -################################################################################ -# `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. - - Parameters - ---------- - - ports : list(int) - List of ports at which the CodeServer's should run. - - pool_port : int - Port at which the server pool should serve. - """ - self.my_port = pool_port - self.ports = ports - queue = Queue(maxsize=len(ports)) - self.queue = queue - servers = [] - for port in ports: - server = CodeServer(port, queue) - servers.append(server) - p = Process(target=server.run) - p.start() - self.servers = servers - - def get_server_port(self): - """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 - - def run(self): - """Run server which returns an available server port where code - can be executed. - """ - server = SimpleXMLRPCServer(("localhost", self.my_port)) - self.server = server - server.register_instance(self) - server.serve_forever() - - -################################################################################ -def main(): - run_as_nobody() - if len(sys.argv) == 1: - ports = SERVER_PORTS - else: - ports = [int(x) for x in sys.argv[1:]] - - server_pool = ServerPool(ports=ports, pool_port=SERVER_POOL_PORT) - server_pool.run() - -if __name__ == '__main__': - main() diff --git a/docs/sample.args b/docs/sample.args deleted file mode 100644 index 4d9f00d..0000000 --- a/docs/sample.args +++ /dev/null @@ -1,2 +0,0 @@ -1 2 -2 1 diff --git a/docs/sample.sh b/docs/sample.sh deleted file mode 100755 index e935cb3..0000000 --- a/docs/sample.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -[[ $# -eq 2 ]] && echo $(( $1 + $2 )) && exit $(( $1 + $2 )) diff --git a/docs/sample_questions.py b/docs/sample_questions.py deleted file mode 100644 index aa7f239..0000000 --- a/docs/sample_questions.py +++ /dev/null @@ -1,59 +0,0 @@ -from datetime import date - -questions = [ -Question( - summary='Factorial', - points=2, - type="python", - description=''' -Write a function called fact which takes a single integer argument -(say n) and returns the factorial of the number. -For example:
-fact(3) -> 6 -''', - test=''' -assert fact(0) == 1 -assert fact(5) == 120 -'''), - -Question( - summary='Simple function', - points=1, - type="python", - description='''Create a simple function called sqr which takes a single -argument and returns the square of the argument. For example:
-sqr(3) -> 9.''', - test=''' -import math -assert sqr(3) == 9 -assert abs(sqr(math.sqrt(2)) - 2.0) < 1e-14 - '''), -Question( - summary='Bash addition', - points=2, - type="bash", - description='''Write a shell script which takes two arguments on the - command line and prints the sum of the two on the output.''', - test='''\ -docs/sample.sh -docs/sample.args -'''), -Question( - summary='Size of integer in Python', - points=0.5, - type="mcq", - description='''What is the largest integer value that can be represented -in Python?''', - options='''No Limit -2**32 -2**32 - 1 -None of the above -''', - test = "No Limit" - ), -] - -quiz = Quiz(start_date=date.today(), - duration=10, - description='Basic Python Quiz 1' - ) diff --git a/docs/sample_questions.xml b/docs/sample_questions.xml deleted file mode 100644 index 53c76f8..0000000 --- a/docs/sample_questions.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - -Factorial - - -Write a function called "fact" which takes a single integer argument (say "n") -and returns the factorial of the number. -For example fact(3) -> 6 - -2 -python - -assert fact(0) == 1 -assert fact(5) == 120 - - - - - - - -Simple function - - -Create a simple function called "sqr" which takes a single argument and -returns the square of the argument -For example sqr(3) -> 9. - -1 -python - -import math -assert sqr(3) == 9 -assert abs(sqr(math.sqrt(2)) - 2.0) < 1e-14 - - - - - - - diff --git a/exam/__init__.py b/exam/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/exam/admin.py b/exam/admin.py deleted file mode 100644 index 8482ef9..0000000 --- a/exam/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from exam.models import Question, Quiz -from django.contrib import admin - -admin.site.register(Question) -admin.site.register(Quiz) diff --git a/exam/forms.py b/exam/forms.py deleted file mode 100644 index a5ca26f..0000000 --- a/exam/forms.py +++ /dev/null @@ -1,95 +0,0 @@ -from django import forms -from exam.models import Profile - -from django.contrib.auth import authenticate -from django.contrib.auth.models import User - -from string import letters, punctuation, digits - -UNAME_CHARS = letters + "._" + digits -PWD_CHARS = letters + punctuation + digits - -class UserRegisterForm(forms.Form): - - username = forms.CharField(max_length=30, - help_text='Letters, digits, period and underscores only.') - email = forms.EmailField() - password = forms.CharField(max_length=30, - widget=forms.PasswordInput()) - confirm_password = forms.CharField(max_length=30, - widget=forms.PasswordInput()) - first_name = forms.CharField(max_length=30) - last_name = forms.CharField(max_length=30) - roll_number = forms.CharField(max_length=30, - help_text="Use a dummy if you don't have one.") - institute = forms.CharField(max_length=128, - help_text='Institute/Organization') - department = forms.CharField(max_length=64, - help_text='Department you work/study at') - position = forms.CharField(max_length=64, - help_text='Student/Faculty/Researcher/Industry/etc.') - - def clean_username(self): - u_name = self.cleaned_data["username"] - - if u_name.strip(UNAME_CHARS): - msg = "Only letters, digits, period and underscore characters are "\ - "allowed in username" - raise forms.ValidationError(msg) - - try: - User.objects.get(username__exact = u_name) - raise forms.ValidationError("Username already exists.") - except User.DoesNotExist: - return u_name - - def clean_password(self): - pwd = self.cleaned_data['password'] - if pwd.strip(PWD_CHARS): - raise forms.ValidationError("Only letters, digits and punctuation are \ - allowed in password") - return pwd - - def clean_confirm_password(self): - c_pwd = self.cleaned_data['confirm_password'] - pwd = self.data['password'] - if c_pwd != pwd: - raise forms.ValidationError("Passwords do not match") - - return c_pwd - - def save(self): - u_name = self.cleaned_data["username"] - u_name = u_name.lower() - pwd = self.cleaned_data["password"] - email = self.cleaned_data['email'] - new_user = User.objects.create_user(u_name, email, pwd) - - new_user.first_name = self.cleaned_data["first_name"] - new_user.last_name = self.cleaned_data["last_name"] - new_user.save() - - cleaned_data = self.cleaned_data - new_profile = Profile(user=new_user) - new_profile.roll_number = cleaned_data["roll_number"] - new_profile.institute = cleaned_data["institute"] - new_profile.department = cleaned_data["department"] - new_profile.position = cleaned_data["position"] - new_profile.save() - - return u_name, pwd - -class UserLoginForm(forms.Form): - username = forms.CharField(max_length = 30) - password = forms.CharField(max_length=30, widget=forms.PasswordInput()) - - def clean(self): - super(UserLoginForm, self).clean() - u_name, pwd = self.cleaned_data["username"], self.cleaned_data["password"] - user = authenticate(username = u_name, password = pwd) - - if not user: - raise forms.ValidationError("Invalid username/password") - - return user - diff --git a/exam/management/__init__.py b/exam/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/exam/management/commands/__init__.py b/exam/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/exam/management/commands/dump_user_data.py b/exam/management/commands/dump_user_data.py deleted file mode 100644 index ec016bb..0000000 --- a/exam/management/commands/dump_user_data.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys - -# Django imports. -from django.core.management.base import BaseCommand -from django.template import Template, Context - -# Local imports. -from exam.views import get_user_data -from exam.models import User - -data_template = Template('''\ -=============================================================================== -Data for {{ data.user.get_full_name.title }} ({{ data.user.username }}) - -Name: {{ data.user.get_full_name.title }} -Username: {{ data.user.username }} -{% if data.profile %}\ -Roll number: {{ data.profile.roll_number }} -Position: {{ data.profile.position }} -Department: {{ data.profile.department }} -Institute: {{ data.profile.institute }} -{% endif %}\ -Email: {{ data.user.email }} -Date joined: {{ data.user.date_joined }} -Last login: {{ data.user.last_login }} -{% for paper in data.papers %} -Paper: {{ paper.quiz.description }} ---------------------------------------- -Marks obtained: {{ paper.get_total_marks }} -Questions correctly answered: {{ paper.get_answered_str }} -Total attempts at questions: {{ paper.answers.count }} -Start time: {{ paper.start_time }} -User IP address: {{ paper.user_ip }} -{% if paper.answers.count %} -Answers -------- -{% for question, answers in paper.get_question_answers.items %} -Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }}) -{% if question.type == "mcq" %}\ -############################################################################### -Choices: {% for option in question.options.strip.splitlines %} {{option}}, {% endfor %} -Student answer: {{ answers.0|safe }} -{% else %}{# non-mcq questions #}\ -{% for answer in answers %}\ -############################################################################### -{{ answer.answer.strip|safe }} -# Autocheck: {{ answer.error|safe }} -{% endfor %}{# for answer in answers #}\ -{% endif %}\ -{% with answers|last as answer %}\ -Marks: {{answer.marks}} -{% endwith %}\ -{% endfor %}{# for question, answers ... #}\ - -Teacher comments ------------------ -{{ paper.comments|default:"None" }} -{% endif %}{# if paper.answers.count #}\ -{% endfor %}{# for paper in data.papers #} -''') - - -def dump_user_data(unames, stdout): - '''Dump user data given usernames (a sequence) if none is given dump all - their data. The data is dumped to stdout. - ''' - if not unames: - try: - users = User.objects.all() - except User.DoesNotExist: - pass - else: - users = [] - for uname in unames: - try: - user = User.objects.get(username__exact = uname) - except User.DoesNotExist: - stdout.write('User %s does not exist'%uname) - else: - users.append(user) - - for user in users: - data = get_user_data(user.username) - context = Context({'data': data}) - result = data_template.render(context) - stdout.write(result.encode('ascii', 'xmlcharrefreplace')) - -class Command(BaseCommand): - args = ' ... ' - help = '''Dumps all user data to stdout, optional usernames can be - specified. If none is specified all user data is dumped. - ''' - - def handle(self, *args, **options): - """Handle the command.""" - # Dump data. - dump_user_data(args, self.stdout) - diff --git a/exam/management/commands/load_exam.py b/exam/management/commands/load_exam.py deleted file mode 100644 index 3f247a1..0000000 --- a/exam/management/commands/load_exam.py +++ /dev/null @@ -1,55 +0,0 @@ -# System library imports. -from os.path import basename - -# Django imports. -from django.core.management.base import BaseCommand - -# Local imports. -from exam.models import Question, Quiz - -def clear_exam(): - """Deactivate all questions from the database.""" - for question in Question.objects.all(): - question.active = False - question.save() - - # Deactivate old quizzes. - for quiz in Quiz.objects.all(): - quiz.active = False - quiz.save() - -def load_exam(filename): - """Load questions and quiz from the given Python file. The Python file - should declare a list of name "questions" which define all the questions - in pure Python. It can optionally load a Quiz from an optional 'quiz' - object. - """ - # Simply exec the given file and we are done. - exec(open(filename).read()) - - if 'questions' not in locals(): - msg = 'No variable named "questions" with the Questions in file.' - raise NameError(msg) - - for question in questions: - question.save() - - if 'quiz' in locals(): - quiz.save() - -class Command(BaseCommand): - args = '' - help = '''loads the questions from given Python files which declare the - questions in a list called "questions".''' - - def handle(self, *args, **options): - """Handle the command.""" - # Delete existing stuff. - clear_exam() - - # Load from files. - for fname in args: - self.stdout.write('Importing from {0} ... '.format(basename(fname))) - load_exam(fname) - self.stdout.write('Done\n') - diff --git a/exam/management/commands/load_questions_xml.py b/exam/management/commands/load_questions_xml.py deleted file mode 100644 index 8bc2701..0000000 --- a/exam/management/commands/load_questions_xml.py +++ /dev/null @@ -1,73 +0,0 @@ -# System library imports. -from os.path import basename -from xml.dom.minidom import parse -from htmlentitydefs import name2codepoint -import re - -# Django imports. -from django.core.management.base import BaseCommand - -# Local imports. -from exam.models import Question - -def decode_html(html_str): - """Un-escape or decode HTML strings to more usable Python strings. - From here: http://wiki.python.org/moin/EscapingHtml - """ - return re.sub('&(%s);' % '|'.join(name2codepoint), - lambda m: unichr(name2codepoint[m.group(1)]), html_str) - -def clear_questions(): - """Deactivate all questions from the database.""" - for question in Question.objects.all(): - question.active = False - question.save() - -def load_questions_xml(filename): - """Load questions from the given XML file.""" - q_bank = parse(filename).getElementsByTagName("question") - - for question in q_bank: - - summary_node = question.getElementsByTagName("summary")[0] - summary = (summary_node.childNodes[0].data).strip() - - desc_node = question.getElementsByTagName("description")[0] - description = (desc_node.childNodes[0].data).strip() - - type_node = question.getElementsByTagName("type")[0] - type = (type_node.childNodes[0].data).strip() - - points_node = question.getElementsByTagName("points")[0] - points = float((points_node.childNodes[0].data).strip()) \ - if points_node else 1.0 - - test_node = question.getElementsByTagName("test")[0] - test = decode_html((test_node.childNodes[0].data).strip()) - - opt_node = question.getElementsByTagName("options")[0] - opt = decode_html((opt_node.childNodes[0].data).strip()) - - new_question = Question(summary=summary, - description=description, - points=points, - options=opt, - type=type, - test=test) - new_question.save() - -class Command(BaseCommand): - args = '' - help = 'loads the questions from given XML files' - - def handle(self, *args, **options): - """Handle the command.""" - # Delete existing stuff. - clear_questions() - - # Load from files. - for fname in args: - self.stdout.write('Importing from {0} ... '.format(basename(fname))) - load_questions_xml(fname) - self.stdout.write('Done\n') - diff --git a/exam/management/commands/results2csv.py b/exam/management/commands/results2csv.py deleted file mode 100644 index 2993745..0000000 --- a/exam/management/commands/results2csv.py +++ /dev/null @@ -1,69 +0,0 @@ -# System library imports. -import sys -from os.path import basename - -# Django imports. -from django.core.management.base import BaseCommand -from django.template import Template, Context - -# Local imports. -from exam.models import Quiz, QuestionPaper - -result_template = Template('''\ -"name","username","rollno","email","answered","total","attempts","position",\ -"department","institute" -{% for paper in papers %}\ -"{{ paper.user.get_full_name.title }}",\ -"{{ paper.user.username }}",\ -"{{ paper.profile.roll_number }}",\ -"{{ paper.user.email }}",\ -"{{ paper.get_answered_str }}",\ -{{ paper.get_total_marks }},\ -{{ paper.answers.count }},\ -"{{ paper.profile.position }}",\ -"{{ paper.profile.department }}",\ -"{{ paper.profile.institute }}" -{% endfor %}\ -''') - -def results2csv(filename, stdout): - """Write exam data to a CSV file. It prompts the user to choose the - appropriate quiz. - """ - qs = Quiz.objects.all() - - if len(qs) > 1: - print "Select quiz to save:" - for q in qs: - stdout.write('%d. %s\n'%(q.id, q.description)) - quiz_id = int(raw_input("Please select quiz: ")) - try: - quiz = Quiz.objects.get(id=quiz_id) - except Quiz.DoesNotExist: - stdout.write("Sorry, quiz %d does not exist!\n"%quiz_id) - sys.exit(1) - else: - quiz = qs[0] - - papers = QuestionPaper.objects.filter(quiz=quiz, - user__profile__isnull=False) - stdout.write("Saving results of %s to %s ... "%(quiz.description, - basename(filename))) - # Render the data and write it out. - f = open(filename, 'w') - context = Context({'papers': papers}) - f.write(result_template.render(context)) - f.close() - - stdout.write('Done\n') - -class Command(BaseCommand): - args = '' - help = '''Writes out the results of a quiz to a CSV file. Prompt user - to select appropriate quiz if there are multiple. - ''' - - def handle(self, *args, **options): - """Handle the command.""" - # Save to file. - results2csv(args[0], self.stdout) diff --git a/exam/migrations/0001_initial.py b/exam/migrations/0001_initial.py deleted file mode 100644 index 49048cc..0000000 --- a/exam/migrations/0001_initial.py +++ /dev/null @@ -1,193 +0,0 @@ -# encoding: utf-8 -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -class Migration(SchemaMigration): - - def forwards(self, orm): - - # Adding model 'Profile' - db.create_table('exam_profile', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True)), - ('roll_number', self.gf('django.db.models.fields.CharField')(max_length=20)), - ('institute', self.gf('django.db.models.fields.CharField')(max_length=128)), - ('department', self.gf('django.db.models.fields.CharField')(max_length=64)), - ('position', self.gf('django.db.models.fields.CharField')(max_length=64)), - )) - db.send_create_signal('exam', ['Profile']) - - # Adding model 'Question' - db.create_table('exam_question', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('summary', self.gf('django.db.models.fields.CharField')(max_length=256)), - ('description', self.gf('django.db.models.fields.TextField')()), - ('points', self.gf('django.db.models.fields.FloatField')(default=1.0)), - ('test', self.gf('django.db.models.fields.TextField')(blank=True)), - ('options', self.gf('django.db.models.fields.TextField')(blank=True)), - ('type', self.gf('django.db.models.fields.CharField')(max_length=24)), - ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), - )) - db.send_create_signal('exam', ['Question']) - - # Adding model 'Answer' - db.create_table('exam_answer', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['exam.Question'])), - ('answer', self.gf('django.db.models.fields.TextField')()), - ('error', self.gf('django.db.models.fields.TextField')()), - ('marks', self.gf('django.db.models.fields.FloatField')(default=0.0)), - ('correct', self.gf('django.db.models.fields.BooleanField')(default=False)), - )) - db.send_create_signal('exam', ['Answer']) - - # Adding model 'Quiz' - db.create_table('exam_quiz', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('start_date', self.gf('django.db.models.fields.DateField')()), - ('duration', self.gf('django.db.models.fields.IntegerField')(default=20)), - ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), - ('description', self.gf('django.db.models.fields.CharField')(max_length=256)), - )) - db.send_create_signal('exam', ['Quiz']) - - # Adding model 'QuestionPaper' - db.create_table('exam_questionpaper', ( - ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), - ('profile', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['exam.Profile'])), - ('quiz', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['exam.Quiz'])), - ('start_time', self.gf('django.db.models.fields.DateTimeField')()), - ('user_ip', self.gf('django.db.models.fields.CharField')(max_length=15)), - ('key', self.gf('django.db.models.fields.CharField')(max_length=10)), - ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), - ('questions', self.gf('django.db.models.fields.CharField')(max_length=128)), - ('questions_answered', self.gf('django.db.models.fields.CharField')(max_length=128)), - ('comments', self.gf('django.db.models.fields.TextField')()), - )) - db.send_create_signal('exam', ['QuestionPaper']) - - # Adding M2M table for field answers on 'QuestionPaper' - db.create_table('exam_questionpaper_answers', ( - ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('questionpaper', models.ForeignKey(orm['exam.questionpaper'], null=False)), - ('answer', models.ForeignKey(orm['exam.answer'], null=False)) - )) - db.create_unique('exam_questionpaper_answers', ['questionpaper_id', 'answer_id']) - - - def backwards(self, orm): - - # Deleting model 'Profile' - db.delete_table('exam_profile') - - # Deleting model 'Question' - db.delete_table('exam_question') - - # Deleting model 'Answer' - db.delete_table('exam_answer') - - # Deleting model 'Quiz' - db.delete_table('exam_quiz') - - # Deleting model 'QuestionPaper' - db.delete_table('exam_questionpaper') - - # Removing M2M table for field answers on 'QuestionPaper' - db.delete_table('exam_questionpaper_answers') - - - models = { - 'auth.group': { - 'Meta': {'object_name': 'Group'}, - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - 'auth.permission': { - 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - 'auth.user': { - 'Meta': {'object_name': 'User'}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - 'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - 'exam.answer': { - 'Meta': {'object_name': 'Answer'}, - 'answer': ('django.db.models.fields.TextField', [], {}), - 'correct': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'error': ('django.db.models.fields.TextField', [], {}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'marks': ('django.db.models.fields.FloatField', [], {'default': '0.0'}), - 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['exam.Question']"}) - }, - 'exam.profile': { - 'Meta': {'object_name': 'Profile'}, - 'department': ('django.db.models.fields.CharField', [], {'max_length': '64'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'institute': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'position': ('django.db.models.fields.CharField', [], {'max_length': '64'}), - 'roll_number': ('django.db.models.fields.CharField', [], {'max_length': '20'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) - }, - 'exam.question': { - 'Meta': {'object_name': 'Question'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'description': ('django.db.models.fields.TextField', [], {}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'options': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'points': ('django.db.models.fields.FloatField', [], {'default': '1.0'}), - 'summary': ('django.db.models.fields.CharField', [], {'max_length': '256'}), - 'test': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'type': ('django.db.models.fields.CharField', [], {'max_length': '24'}) - }, - 'exam.questionpaper': { - 'Meta': {'object_name': 'QuestionPaper'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'answers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['exam.Answer']", 'symmetrical': 'False'}), - 'comments': ('django.db.models.fields.TextField', [], {}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'key': ('django.db.models.fields.CharField', [], {'max_length': '10'}), - 'profile': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['exam.Profile']"}), - 'questions': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'questions_answered': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'quiz': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['exam.Quiz']"}), - 'start_time': ('django.db.models.fields.DateTimeField', [], {}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), - 'user_ip': ('django.db.models.fields.CharField', [], {'max_length': '15'}) - }, - 'exam.quiz': { - 'Meta': {'object_name': 'Quiz'}, - 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'description': ('django.db.models.fields.CharField', [], {'max_length': '256'}), - 'duration': ('django.db.models.fields.IntegerField', [], {'default': '20'}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'start_date': ('django.db.models.fields.DateField', [], {}) - } - } - - complete_apps = ['exam'] diff --git a/exam/migrations/__init__.py b/exam/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/exam/models.py b/exam/models.py deleted file mode 100644 index 717e02e..0000000 --- a/exam/models.py +++ /dev/null @@ -1,221 +0,0 @@ -import datetime -from django.db import models -from django.contrib.auth.models import User - -################################################################################ -class Profile(models.Model): - """Profile for a user to store roll number and other details.""" - user = models.OneToOneField(User) - roll_number = models.CharField(max_length=20) - institute = models.CharField(max_length=128) - department = models.CharField(max_length=64) - position = models.CharField(max_length=64) - - -QUESTION_TYPE_CHOICES = ( - ("python", "Python"), - ("bash", "Bash"), - ("mcq", "MultipleChoice"), - ) - -################################################################################ -class Question(models.Model): - """A question in the database.""" - - # A one-line summary of the question. - summary = models.CharField(max_length=256) - - # The question text, should be valid HTML. - description = models.TextField() - - # Number of points for the question. - points = models.FloatField(default=1.0) - - # Test cases for the question in the form of code that is run. - test = models.TextField(blank=True) - - # Any multiple choice options. Place one option per line. - options = models.TextField(blank=True) - - # The type of question. - type = models.CharField(max_length=24, choices=QUESTION_TYPE_CHOICES) - - # Is this question active or not. If it is inactive it will not be used - # when creating a QuestionPaper. - active = models.BooleanField(default=True) - - def __unicode__(self): - return self.summary - - -################################################################################ -class Answer(models.Model): - """Answers submitted by users. - """ - # The question for which we are an answer. - question = models.ForeignKey(Question) - - # The answer submitted by the user. - answer = models.TextField() - - # Error message when auto-checking the answer. - error = models.TextField() - - # Marks obtained for the answer. This can be changed by the teacher if the - # grading is manual. - marks = models.FloatField(default=0.0) - - # Is the answer correct. - correct = models.BooleanField(default=False) - - def __unicode__(self): - return self.answer - -################################################################################ -class Quiz(models.Model): - """A quiz that students will participate in. One can think of this - as the "examination" event. - """ - - # The starting/ending date of the quiz. - start_date = models.DateField("Date of the quiz") - - # This is always in minutes. - duration = models.IntegerField("Duration of quiz in minutes", default=20) - - # Is the quiz active. The admin should deactivate the quiz once it is - # complete. - active = models.BooleanField(default=True) - - # Description of quiz. - description = models.CharField(max_length=256) - - class Meta: - verbose_name_plural = "Quizzes" - - def __unicode__(self): - desc = self.description or 'Quiz' - return '%s: on %s for %d minutes'%(desc, self.start_date, self.duration) - - -################################################################################ -class QuestionPaper(models.Model): - """A question paper for a student -- one per student typically. - """ - # The user taking this question paper. - user = models.ForeignKey(User) - - # The user's profile, we store a reference to make it easier to access the - # data. - profile = models.ForeignKey(Profile) - - # The Quiz to which this question paper is attached to. - quiz = models.ForeignKey(Quiz) - - # The time when this paper was started by the user. - start_time = models.DateTimeField() - - # User's IP which is logged. - user_ip = models.CharField(max_length=15) - # Unused currently. - key = models.CharField(max_length=10) - - # used to allow/stop a user from retaking the question paper. - active = models.BooleanField(default = True) - - # The questions (a list of ids separated by '|') - questions = models.CharField(max_length=128) - # The questions successfully answered (a list of ids separated by '|') - questions_answered = models.CharField(max_length=128) - - # All the submitted answers. - answers = models.ManyToManyField(Answer) - - # Teacher comments on the question paper. - comments = models.TextField() - - def current_question(self): - """Returns the current active question to display.""" - qs = self.questions.split('|') - if len(qs) > 0: - return qs[0] - else: - return '' - - def questions_left(self): - """Returns the number of questions left.""" - qs = self.questions - if len(qs) == 0: - return 0 - else: - return qs.count('|') + 1 - - def completed_question(self, question_id): - """Removes the question from the list of questions and returns - the next.""" - qa = self.questions_answered - if len(qa) > 0: - self.questions_answered = '|'.join([qa, str(question_id)]) - else: - self.questions_answered = str(question_id) - qs = self.questions.split('|') - qs.remove(unicode(question_id)) - self.questions = '|'.join(qs) - self.save() - if len(qs) == 0: - return '' - else: - return qs[0] - - def skip(self): - """Skip the current question and return the next available question.""" - qs = self.questions.split('|') - if len(qs) == 0: - return '' - else: - # Put head at the end. - head = qs.pop(0) - qs.append(head) - self.questions = '|'.join(qs) - self.save() - return qs[0] - - def time_left(self): - """Return the time remaining for the user in seconds.""" - dt = datetime.datetime.now() - self.start_time - try: - secs = dt.total_seconds() - except AttributeError: - # total_seconds is new in Python 2.7. :( - secs = dt.seconds + dt.days*24*3600 - total = self.quiz.duration*60.0 - remain = max(total - secs, 0) - return int(remain) - - def get_answered_str(self): - """Returns the answered questions, sorted and as a nice string.""" - qa = self.questions_answered.split('|') - answered = ', '.join(sorted(qa)) - return answered if answered else 'None' - - def get_total_marks(self): - """Returns the total marks earned by student for this paper.""" - return sum([x.marks for x in self.answers.filter(marks__gt=0.0)]) - - def get_question_answers(self): - """Return a dictionary with keys as questions and a list of the corresponding - answers. - """ - q_a = {} - for answer in self.answers.all(): - question = answer.question - if question in q_a: - q_a[question].append(answer) - else: - q_a[question] = [answer] - return q_a - - def __unicode__(self): - u = self.user - return u'Question paper for {0} {1}'.format(u.first_name, u.last_name) - diff --git a/exam/tests.py b/exam/tests.py deleted file mode 100644 index 501deb7..0000000 --- a/exam/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/exam/urls.py b/exam/urls.py deleted file mode 100644 index 34e329f..0000000 --- a/exam/urls.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.conf.urls.defaults import patterns, include, url - -urlpatterns = patterns('exam.views', - url(r'^$', 'index'), - url(r'^login/$', 'user_login'), - url(r'^register/$', 'user_register'), - url(r'^start/$', 'start'), - url(r'^quit/$', 'quit'), - url(r'^complete/$', 'complete'), - url(r'^monitor/$', 'monitor'), - url(r'^monitor/(?P\d+)/$', 'monitor'), - url(r'^user_data/(?P[a-zA-Z0-9_.]+)/$', 'user_data'), - url(r'^grade_user/(?P[a-zA-Z0-9_.]+)/$', 'grade_user'), - url(r'^(?P\d+)/$', 'question'), - url(r'^(?P\d+)/check/$', 'check'), -) diff --git a/exam/views.py b/exam/views.py deleted file mode 100644 index c178a0b..0000000 --- a/exam/views.py +++ /dev/null @@ -1,351 +0,0 @@ -import random -import string -import os -import stat -from os.path import dirname, pardir, abspath, join, exists -import datetime - -from django.contrib.auth import login, logout, authenticate -from django.shortcuts import render_to_response, get_object_or_404, redirect -from django.template import RequestContext -from django.http import Http404 -from django.db.models import Sum - -# Local imports. -from exam.models import Quiz, Question, QuestionPaper, Profile, Answer, User -from exam.forms import UserRegisterForm, UserLoginForm -from exam.xmlrpc_clients import code_server -from settings import URL_ROOT - -# The directory where user data can be saved. -OUTPUT_DIR = abspath(join(dirname(__file__), pardir, 'output')) - - -def my_redirect(url): - """An overridden redirect to deal with URL_ROOT-ing. See settings.py - for details.""" - return redirect(URL_ROOT + url) - -def my_render_to_response(template, context=None, **kwargs): - """Overridden render_to_response. - """ - if context is None: - context = {'URL_ROOT': URL_ROOT} - else: - context['URL_ROOT'] = URL_ROOT - return render_to_response(template, context, **kwargs) - - -def gen_key(no_of_chars): - """Generate a random key of the number of characters.""" - allowed_chars = string.digits+string.uppercase - return ''.join([random.choice(allowed_chars) for i in range(no_of_chars)]) - -def get_user_dir(user): - """Return the output directory for the user.""" - user_dir = join(OUTPUT_DIR, str(user.username)) - if not exists(user_dir): - os.mkdir(user_dir) - # Make it rwx by others. - os.chmod(user_dir, stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH \ - | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR \ - | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP) - return user_dir - -def index(request): - """The start page. - """ - user = request.user - if user.is_authenticated(): - return my_redirect("/exam/start/") - - return my_redirect("/exam/login/") - -def user_register(request): - """ Register a new user. - Create a user and corresponding profile and store roll_number also.""" - - user = request.user - if user.is_authenticated(): - return my_redirect("/exam/start/") - - if request.method == "POST": - form = UserRegisterForm(request.POST) - if form.is_valid(): - data = form.cleaned_data - u_name, pwd = form.save() - - new_user = authenticate(username = u_name, password = pwd) - login(request, new_user) - return my_redirect("/exam/start/") - - else: - return my_render_to_response('exam/register.html', - {'form':form}, - context_instance=RequestContext(request)) - else: - form = UserRegisterForm() - return my_render_to_response('exam/register.html', - {'form':form}, - context_instance=RequestContext(request)) - -def user_login(request): - """Take the credentials of the user and log the user in.""" - - user = request.user - if user.is_authenticated(): - return my_redirect("/exam/start/") - - if request.method == "POST": - form = UserLoginForm(request.POST) - if form.is_valid(): - user = form.cleaned_data - login(request, user) - return my_redirect("/exam/start/") - else: - context = {"form": form} - return my_render_to_response('exam/login.html', context, - context_instance=RequestContext(request)) - else: - form = UserLoginForm() - context = {"form": form} - return my_render_to_response('exam/login.html', context, - context_instance=RequestContext(request)) - -def start(request): - user = request.user - try: - # Right now the app is designed so there is only one active quiz - # at a particular time. - quiz = Quiz.objects.get(active=True) - except Quiz.DoesNotExist: - msg = 'No active quiz found, please contact your '\ - 'instructor/administrator. Please login again thereafter.' - return complete(request, reason=msg) - try: - old_paper = QuestionPaper.objects.get(user=user, quiz=quiz) - q = old_paper.current_question() - return show_question(request, q) - except QuestionPaper.DoesNotExist: - ip = request.META['REMOTE_ADDR'] - key = gen_key(10) - try: - profile = user.get_profile() - except Profile.DoesNotExist: - msg = 'You do not have a profile and cannot take the quiz!' - raise Http404(msg) - - new_paper = QuestionPaper(user=user, user_ip=ip, key=key, - quiz=quiz, profile=profile) - new_paper.start_time = datetime.datetime.now() - - # Make user directory. - user_dir = get_user_dir(user) - - questions = [ str(_.id) for _ in Question.objects.filter(active=True) ] - random.shuffle(questions) - - new_paper.questions = "|".join(questions) - new_paper.save() - - # Show the user the intro page. - context = {'user': user} - ci = RequestContext(request) - return my_render_to_response('exam/intro.html', context, - context_instance=ci) - -def question(request, q_id): - user = request.user - if not user.is_authenticated(): - return my_redirect('/exam/login/') - q = get_object_or_404(Question, pk=q_id) - try: - paper = QuestionPaper.objects.get(user=request.user, quiz__active=True) - except QuestionPaper.DoesNotExist: - return my_redirect('/exam/start') - if not paper.quiz.active: - return complete(request, reason='The quiz has been deactivated!') - - time_left = paper.time_left() - if time_left == 0: - return complete(request, reason='Your time is up!') - quiz_name = paper.quiz.description - context = {'question': q, 'paper': paper, 'user': user, - 'quiz_name': quiz_name, - 'time_left': time_left} - ci = RequestContext(request) - return my_render_to_response('exam/question.html', context, - context_instance=ci) - -def show_question(request, q_id): - """Show a question if possible.""" - if len(q_id) == 0: - msg = 'Congratulations! You have successfully completed the quiz.' - return complete(request, msg) - else: - return question(request, q_id) - -def check(request, q_id): - user = request.user - if not user.is_authenticated(): - return my_redirect('/exam/login/') - question = get_object_or_404(Question, pk=q_id) - paper = QuestionPaper.objects.get(user=user, quiz__active=True) - answer = request.POST.get('answer') - skip = request.POST.get('skip', None) - - if skip is not None: - next_q = paper.skip() - return show_question(request, next_q) - - # Add the answer submitted, regardless of it being correct or not. - new_answer = Answer(question=question, answer=answer, correct=False) - new_answer.save() - 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 - # safely in a separate process (the code_server.py) running as nobody. - if question.type == 'mcq': - success = True # Only one attempt allowed for MCQ's. - if answer.strip() == question.test.strip(): - new_answer.correct = True - new_answer.marks = question.points - new_answer.error = 'Correct answer' - else: - new_answer.error = 'Incorrect answer' - else: - user_dir = get_user_dir(user) - success, err_msg = code_server.run_code(answer, question.test, - user_dir, question.type) - new_answer.error = err_msg - if success: - # Note the success and save it along with the marks. - new_answer.correct = success - new_answer.marks = question.points - - new_answer.save() - - if not success: # Should only happen for non-mcq questions. - time_left = paper.time_left() - if time_left == 0: - return complete(request, reason='Your time is up!') - if not paper.quiz.active: - return complete(request, reason='The quiz has been deactivated!') - - context = {'question': question, 'error_message': err_msg, - 'paper': paper, 'last_attempt': answer, - 'quiz_name': paper.quiz.description, - 'time_left': time_left} - ci = RequestContext(request) - - return my_render_to_response('exam/question.html', context, - context_instance=ci) - else: - next_q = paper.completed_question(question.id) - return show_question(request, next_q) - -def quit(request): - return my_render_to_response('exam/quit.html', - context_instance=RequestContext(request)) - -def complete(request, reason=None): - user = request.user - no = False - message = reason or 'The quiz has been completed. Thank you.' - if request.method == 'POST' and 'no' in request.POST: - no = request.POST.get('no', False) - if not no: - # Logout the user and quit with the message given. - logout(request) - context = {'message': message} - return my_render_to_response('exam/complete.html', context) - else: - return my_redirect('/exam/') - - -def monitor(request, quiz_id=None): - """Monitor the progress of the papers taken so far.""" - user = request.user - if not user.is_authenticated() and not user.is_staff: - raise Http404('You are not allowed to view this page!') - - if quiz_id is None: - quizzes = Quiz.objects.all() - context = {'papers': [], - 'quiz': None, - 'quizzes':quizzes} - return my_render_to_response('exam/monitor.html', context, - context_instance=RequestContext(request)) - # quiz_id is not None. - try: - quiz = Quiz.objects.get(id=quiz_id) - except Quiz.DoesNotExist: - papers = [] - quiz = None - else: - papers = QuestionPaper.objects.all().annotate( - total=Sum('answers__marks')).order_by('-total') - - context = {'papers': papers, 'quiz': quiz, 'quizzes': None} - return my_render_to_response('exam/monitor.html', context, - context_instance=RequestContext(request)) - -def get_user_data(username): - """For a given username, this returns a dictionary of important data - related to the user including all the user's answers submitted. - """ - user = User.objects.get(username=username) - papers = QuestionPaper.objects.filter(user=user) - - data = {} - try: - profile = user.get_profile() - except Profile.DoesNotExist: - # Admin user may have a paper by accident but no profile. - profile = None - data['user'] = user - data['profile'] = profile - data['papers'] = papers - return data - -def user_data(request, username): - """Render user data.""" - current_user = request.user - if not current_user.is_authenticated() and not current_user.is_staff: - raise Http404('You are not allowed to view this page!') - - data = get_user_data(username) - - context = {'data': data} - return my_render_to_response('exam/user_data.html', context, - context_instance=RequestContext(request)) - -def grade_user(request, username): - """Present an interface with which we can easily grade a user's papers - and update all their marks and also give comments for each paper. - """ - current_user = request.user - if not current_user.is_authenticated() and not current_user.is_staff: - raise Http404('You are not allowed to view this page!') - - data = get_user_data(username) - if request.method == 'POST': - papers = data['papers'] - for paper in papers: - for question, answers in paper.get_question_answers().iteritems(): - marks = float(request.POST.get('q%d_marks'%question.id)) - last_ans = answers[-1] - last_ans.marks = marks - last_ans.save() - paper.comments = request.POST.get('comments_%d'%paper.quiz.id) - paper.save() - - context = {'data': data} - return my_render_to_response('exam/user_data.html', context, - context_instance=RequestContext(request)) - else: - context = {'data': data} - return my_render_to_response('exam/grade_user.html', context, - context_instance=RequestContext(request)) - diff --git a/exam/xmlrpc_clients.py b/exam/xmlrpc_clients.py deleted file mode 100644 index 817e37d..0000000 --- a/exam/xmlrpc_clients.py +++ /dev/null @@ -1,78 +0,0 @@ -from xmlrpclib import ServerProxy -import time -import random -import socket - -from settings import SERVER_PORTS, SERVER_POOL_PORT - - -class ConnectionError(Exception): - pass - -################################################################################ -# `CodeServerProxy` class. -################################################################################ -class CodeServerProxy(object): - """A class that manages accesing the farm of Python servers and making - calls to them such that no one XMLRPC server is overloaded. - """ - def __init__(self): - pool_url = 'http://localhost:%d'%(SERVER_POOL_PORT) - self.pool_server = ServerProxy(pool_url) - self.methods = {"python": 'run_python_code', - "bash": 'run_bash_code'} - - def run_code(self, answer, test_code, user_dir, language): - """Tests given code (`answer`) with the `test_code` supplied. If the - optional `in_dir` keyword argument is supplied it changes the directory - to that directory (it does not change it back to the original when - done). The parameter language specifies which language to use for the - tests. - - Parameters - ---------- - answer : str - The user's answer for the question. - test_code : str - The test code to check the user code with. - user_dir : str (directory) - The directory to run the tests inside. - language : str - The programming language to use. - - Returns - ------- - A tuple: (success, error message). - """ - method_name = self.methods[language] - try: - server = self._get_server() - method = getattr(server, method_name) - result = method(answer, test_code, user_dir) - except ConnectionError: - result = [False, 'Unable to connect to any code servers!'] - 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!") - proxy = ServerProxy('http://localhost:%d'%port) - return proxy - -# views.py calls this Python server which forwards the request to one -# of the running servers. -code_server = CodeServerProxy() - diff --git a/manage.py b/manage.py deleted file mode 100755 index 3e4eedc..0000000 --- a/manage.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -import imp -try: - imp.find_module('settings') # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) - sys.exit(1) - -import settings - -if __name__ == "__main__": - execute_manager(settings) diff --git a/output/README.txt b/output/README.txt deleted file mode 100644 index 3163ed4..0000000 --- a/output/README.txt +++ /dev/null @@ -1,4 +0,0 @@ -This directory contains files generated/saved by users as per their -username. The test executor will chdir into this user directory for each -user when they run the test. Do not delete this directory and ensure that -it is writeable by all. \ No newline at end of file diff --git a/settings.py b/settings.py deleted file mode 100644 index f1c48b9..0000000 --- a/settings.py +++ /dev/null @@ -1,188 +0,0 @@ -# Django settings for tester project. - -from os.path import dirname, join, basename, abspath - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -# 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 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 -# service is running. It should be > 1024 and less < 65535 though. -SERVER_POOL_PORT = 53579 - -# Timeout for the code to run in seconds. This is an integer! -SERVER_TIMEOUT = 2 - -# The root of the URL, for example you might be in the situation where you -# are not hosted as host.org/exam/ but as host.org/foo/exam/ for whatever -# reason set this to the root you have to serve at. In the above example -# host.org/foo/exam set URL_ROOT='/foo' -URL_ROOT = '' - - -ADMINS = ( - # ('Your Name', 'your_email@example.com'), -) - -MANAGERS = ADMINS - -CURDIR = abspath(dirname(__file__)) -DB_FILE = join(CURDIR, 'exam.db') - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': DB_FILE, # Or path to database file if using sqlite3. - 'USER': '', # Not used with sqlite3. - 'PASSWORD': '', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. - } -} - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'Asia/Kolkata' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/home/media/media.lawrence.com/media/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" -MEDIA_URL = '' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" -STATIC_ROOT = '/tmp/static/' - -# URL prefix for static files. -# Example: "http://media.lawrence.com/static/" -STATIC_URL = '/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = URL_ROOT + '/static/admin/' - -# Additional locations of static files -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - join(CURDIR, 'static'), -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = '9h*01@*#3ok+lbj5k=ym^eb)e=rf-g70&n0^nb_q6mtk!r(qr)' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) - -ROOT_URLCONF = '%s.urls'%(basename(CURDIR)) - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - join(CURDIR, "templates"), -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - # Uncomment the next line to enable the admin: - 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', - 'south', - 'exam', -) - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - }, - 'handlers': { - 'console':{ - 'level':'DEBUG', - 'class':'logging.StreamHandler', - 'formatter': 'simple' - }, - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins', 'console'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - -AUTH_PROFILE_MODULE = 'exam.Profile' diff --git a/static/exam/css/base.css b/static/exam/css/base.css deleted file mode 100644 index 1323116..0000000 --- a/static/exam/css/base.css +++ /dev/null @@ -1,26 +0,0 @@ -body { font-family: 'Georgia', serif; font-size: 17px; color: #000; background: #eee;} -h1, h2, h3, h4 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; } -h1 { margin: 0 0 30px 0; font-size: 36px;} -h1 span { display: none; } -h2 { font-size: 26px; margin: 15px 0 5px 0; } -h3 { font-size: 22px; margin: 15px 0 5px 0; } -h4 { font-size: 15px; margin: 15px 0 5px 0; } - -.box { width: 700px; margin: 10px auto ; } -.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; - padding: 0.8em; background: white; } -.entries { list-style: none; margin: 0; padding: 0; } -.entries li { margin: 0.8em 1.2em; } -.entries li h2 { margin-left: -1em; } -.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } -.add-entry dl { font-weight: bold; } -.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; - margin-bottom: 1em; background: #fafafa; } -.flash { background: #CEE5F5; padding: 0.5em; - border: 1px solid #AACBE2; } -.error { background: #F0D6D6; padding: 0.5em; } -textarea, code, -pre { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', - monospace!important; font-size: 14px; background: #eee; } -pre { padding: 0px 30px; margin: 15px -30px; line-height: 1.3; } - diff --git a/templates/404.html b/templates/404.html deleted file mode 100644 index 7d33dd3..0000000 --- a/templates/404.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -The requested page does not exist. -{% endblock %} diff --git a/templates/500.html b/templates/500.html deleted file mode 100644 index d02721f..0000000 --- a/templates/500.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -Internal Server error.
-This event will be reported.
-Sorry for the inconvinience. -{% endblock %} diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index c2bbabb..0000000 --- a/templates/base.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - -{% block title %} -{% endblock %} - -{% block meta %} -{% endblock %} - -{% block css %} -{% endblock %} -{% block script %} -{% endblock %} - - - -
-{% block content %} -{% endblock %} -
- - diff --git a/templates/exam/complete.html b/templates/exam/complete.html deleted file mode 100644 index 4c3f3d5..0000000 --- a/templates/exam/complete.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Good bye!{% endblock %} - -{% block content %} -

Good bye!

- -

{{message}}

-
-

You may now close the browser.

- -{% endblock content %} diff --git a/templates/exam/grade_user.html b/templates/exam/grade_user.html deleted file mode 100644 index 75ed2e0..0000000 --- a/templates/exam/grade_user.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "base.html" %} - -{% block title %} Grading papers for {{ data.user.get_full_name.title }} {% endblock title %} - -{% block content %} - -

Grading papers for {{ data.user.get_full_name.title }}

- -

-Name: {{ data.user.get_full_name.title }} -{% if data.profile %} -(roll number: {{ data.profile.roll_number }})
-{{ data.profile.position }}, -{{ data.profile.department }}, -{{ data.profile.institute }} -{% endif %} -

- -{% if data.papers %} - -{% for paper in data.papers %} - -

Quiz: {{ paper.quiz.description }}

- -

-Questions correctly answered: {{ paper.get_answered_str }}
-Total attempts at questions: {{ paper.answers.count }}
-Marks obtained: {{ paper.get_total_marks }}
-Start time: {{ paper.start_time }}
-

- -{% if paper.answers.count %} -

Answers

-
-{% csrf_token %} -{% for question, answers in paper.get_question_answers.items %} -

- - Question: {{ question.id }}. {{ question.summary }} - (Points: {{ question.points }})

-{% if question.type == "mcq" %} -

Choices: -{% for option in question.options.strip.splitlines %} {{option}}, {% endfor %} -

-

Student answer: {{ answers.0 }}

-{% else %}{# non-mcq questions #} -
-{% for answer in answers %}################################################################################
-{{ answer.answer.strip }}
-# Autocheck: {{ answer.error }}
-{% endfor %}
-{% endif %} {# if question.type #} -{% with answers|last as answer %} -Marks: -{% endwith %} -{% endfor %} {# for question, answers ... #} -

Teacher comments:

- -
- -
-{% endif %} {# if paper.answers.count #} - -{% endfor %} {# for paper in data.papers #} - -{% endif %} {# if data.papers #} - -{% if data.papers.count > 1 %} - - Monitor quiz -{% else %} -{% with data.papers.0 as paper %} - - Monitor quiz -{% endwith %} -{% endif %} -
-Admin -{% endblock content %} diff --git a/templates/exam/intro.html b/templates/exam/intro.html deleted file mode 100644 index 1d3e5de..0000000 --- a/templates/exam/intro.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Instructions and Rules {% endblock %} - -{% block content %} -

Important rules and instructions

- -

Welcome {{user.first_name.title}} {{user.last_name.title}}, -to the programming quiz!

- -

-This examination system has been developed with the intention of making you -learn programming and be assessed in an interactive and fun manner. -You will be presented with a series of programming questions and problems that -you will answer online and get immediate feedback for. -

- -

Here are some important instructions and rules that you should understand -carefully. -

- -
    - -
  • For any programming questions, you can submit solutions as many times as - you want without a penalty. You may skip questions and solve them later. -
  • - -
  • You may use your computer's Python/IPython shell or - an editor to solve - the problem and cut/paste the solution to the web interface. -
  • - -
  • You are not allowed to use any internet - resources, i.e. no google etc.
  • - -
  • Do not copy or share the questions or answers with anyone until the - exam is complete for everyone.
  • - -
  • All your attempts at the questions are logged. - Do not try to outsmart and break the testing system. If you do, we know - who you are and we will expell you from the course. You have been warned. -
  • - -
- -

We hope you enjoy taking this exam.

- -
-{% csrf_token %} - -
- -{% endblock content %} \ No newline at end of file diff --git a/templates/exam/login.html b/templates/exam/login.html deleted file mode 100644 index 8e6352e..0000000 --- a/templates/exam/login.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Login{% endblock title %} - -{% block content %} -

Welcome to the Examination. -Please login to proceed.

- -
-{% csrf_token %} - - -{{ form.as_table }} -
- - -
- -New User Registration -{% endblock content %} \ No newline at end of file diff --git a/templates/exam/monitor.html b/templates/exam/monitor.html deleted file mode 100644 index fb6cb58..0000000 --- a/templates/exam/monitor.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends "base.html" %} - -{% block title %} Quiz results {% endblock title %} - -{% block meta %} {% endblock meta %} - -{% block content %} - -{% if not quizzes and not quiz %} -

Quiz results

- -

No quizzes available.

- -{% endif %} - -{# ############################################################### #} -{# This is rendered when we are just viewing exam/monitor #} -{% if quizzes %} -

Available quizzes

- - -{% endif %} - -{# ############################################################### #} -{# This is rendered when we are just viewing exam/monitor/quiz_num #} -{% if quiz %} -

{{ quiz.description }} results

-{% if papers %} -{#

Quiz: {{ quiz_name }}

#} -

Number of papers: {{ papers|length }}

- - - - - - - - - - - - {% for paper in papers %} - - - - - - - - - - {% endfor %} -
Name Username Roll number Institute Questions answered Total marks Attempts
- {{ paper.user.get_full_name.title }} - {{ paper.user.username }} {{ paper.profile.roll_number }} {{ paper.profile.institute }} {{ paper.get_answered_str }} {{ paper.get_total_marks }} {{ paper.answers.count }}
-{% else %} -

No answer papers so far.

-{% endif %} {# if papers #} -{% endif %} - -Admin - -{% endblock content %} diff --git a/templates/exam/question.html b/templates/exam/question.html deleted file mode 100644 index 8b589b6..0000000 --- a/templates/exam/question.html +++ /dev/null @@ -1,91 +0,0 @@ -{% extends "base.html" %} - -{% block title %} Answer question {% endblock %} - -{% block script %} - -{% endblock script %} - -{% block onload %} onload="update_time()" {% endblock %} - -{% block content %} -

{{ question.summary }}

- -

{{ question.description|safe }} -
-(Marks: {{ question.points }})

- -{% if error_message %}

ERROR:

{{ error_message }}
{% endif %} - -

- -
-{% csrf_token %} -{% if question.type == "mcq" %} -{% for option in question.options.strip.splitlines %} -{{option}}
-{% endfor %} -{% else %} - -{% endif %} -
-{% if question.type == "mcq" %} - -{% else %} - -{% endif %} - -
- -

{{ user.first_name.title }} {{ user.last_name.title }}, -you have {{ paper.questions_left }} question(s) left in {{ quiz_name }}.

- -

Time left:

- -
-
-{% csrf_token %} - -
- -{% endblock content %} diff --git a/templates/exam/quit.html b/templates/exam/quit.html deleted file mode 100644 index 37b5c08..0000000 --- a/templates/exam/quit.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Quit exam {% endblock %} - -{% block content %} -

Your current answers are saved.

-

Are you sure you wish to quit the exam?

- -
-{% csrf_token %} - - -
-{% endblock content %} \ No newline at end of file diff --git a/templates/exam/register.html b/templates/exam/register.html deleted file mode 100644 index 921e7b5..0000000 --- a/templates/exam/register.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Registration form {% endblock %} - -{% block content %} -Please provide the following details. -
-{% csrf_token %} - - -{{ form.as_table }} -
- - -
- -{% endblock content %} \ No newline at end of file diff --git a/templates/exam/user_data.html b/templates/exam/user_data.html deleted file mode 100644 index 9fb442a..0000000 --- a/templates/exam/user_data.html +++ /dev/null @@ -1,84 +0,0 @@ -{% extends "base.html" %} - -{% block title %} Data for user {{ data.user.get_full_name.title }} {% endblock title %} - -{% block content %} - -

Data for user {{ data.user.get_full_name.title }}

- -

-Name: {{ data.user.get_full_name.title }}
-Username: {{ data.user.username }}
-{% if data.profile %} -Roll number: {{ data.profile.roll_number }}
-Position: {{ data.profile.position }}
-Department: {{ data.profile.department }}
-Institute: {{ data.profile.institute }}
-{% endif %} -Email: {{ data.user.email }}
-Date joined: {{ data.user.date_joined }}
-Last login: {{ data.user.last_login }} -

- -{% if data.papers %} -

- Grade/correct paper -

- -{% for paper in data.papers %} - -

Quiz: {{ paper.quiz.description }}

- -

-Questions correctly answered: {{ paper.get_answered_str }}
-Total attempts at questions: {{ paper.answers.count }}
-Marks obtained: {{ paper.get_total_marks }}
-Start time: {{ paper.start_time }}
-User IP address: {{ paper.user_ip }} -

- -{% if paper.answers.count %} -

Answers

-{% for question, answers in paper.get_question_answers.items %} -

Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})

-{% if question.type == "mcq" %} -

Choices: -{% for option in question.options.strip.splitlines %} {{option}}, {% endfor %} -

-

Student answer: {{ answers.0 }}

-{% else %}{# non-mcq questions #} -
 
-{% for answer in answers %}################################################################################
-{{ answer.answer.strip }}
-# Autocheck: {{ answer.error }}
-{% endfor %}
-{% endif %} -{% with answers|last as answer %} -

Marks: {{answer.marks}}

-{% endwith %} -{% endfor %} {# for question, answers ... #} -

Teacher comments:

-{{ paper.comments|default:"None" }} -{% endif %} {# if paper.answers.count #} - -{% endfor %} {# for paper in data.papers #} - -{% endif %} {# if data.papers #} -
-
- - Grade/correct paper -
-{% if data.papers.count > 1 %} - - Monitor quiz -{% else %} -{% with data.papers.0 as paper %} - - Monitor quiz -{% endwith %} -{% endif %} -
-Admin - -{% endblock content %} diff --git a/test_server.py b/test_server.py deleted file mode 100644 index be9f876..0000000 --- a/test_server.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Simple test suite for the code server. Running this requires that one start -up the code server as:: - - $ sudo ./code_server.py - -""" -from exam.xmlrpc_clients import code_server - -def check_result(result, check='correct answer'): - if check != 'correct answer': - assert result[0] == False - else: - assert result[0] == True - if "unable to connect" in result[1].lower(): - assert result[0], result[1] - assert check in result[1].lower(), result[1] - -def test_python(): - """Test if server runs Python code as expected.""" - src = 'while True: pass' - result = code_server.run_code(src, '', '/tmp', language="python") - check_result(result, 'more than ') - - src = 'x = 1' - result = code_server.run_code(src, 'assert x == 1', '/tmp', - language="python") - check_result(result, 'correct answer') - - result = code_server.run_code(src, 'assert x == 0', '/tmp', - language="python") - check_result(result, 'assertionerror') - - src = 'abracadabra' - result = code_server.run_code(src, 'assert x == 0', '/tmp', - language="python") - check_result(result, 'nameerror') - -def test_bash(): - """Test if server runs Bash code as expected.""" - src = """ -#!/bin/bash - [[ $# -eq 2 ]] && echo $(( $1 + $2 )) && exit $(( $1 + $2 )) - """ - result = code_server.run_code(src, - 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") - check_result(result) - - src = """ -#!/bin/bash - [[ $# -eq 2 ]] && echo $(( $1 - $2 )) && exit $(( $1 - $2 )) - """ - result = code_server.run_code(src, - 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") - check_result(result, 'error') - - src = """\ -#!/bin/bash - while [ 1 ] ; do echo "" > /dev/null ; done - """ - result = code_server.run_code(src, - 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") - check_result(result, 'more than ') - - src = ''' -#!/bin/bash - while [ 1 ] ; do echo "" > /dev/null - ''' - result = code_server.run_code(src, - 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") - check_result(result, 'error') - - src = '''# Enter your code here. -#!/bin/bash - while [ 1 ] ; do echo "" > /dev/null - ''' - result = code_server.run_code(src, - 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") - check_result(result, 'oserror') - -if __name__ == '__main__': - test_python() - test_bash() - diff --git a/testapp/README.txt b/testapp/README.txt new file mode 100644 index 0000000..a265675 --- /dev/null +++ b/testapp/README.txt @@ -0,0 +1,190 @@ +Introduction +============ + +This app provides an "exam" app that lets users take an online +programming quiz. Currently only Python and simple Bash scripts can be +tested. At FOSSEE, Nishanth had implemented a nice django based app to +test for multiple-choice questions. However, I was inspired by a +programming contest that I saw at PyCon APAC 2011. Chris Boesch, who +administered the contest, used a nice web application that he had built +on top of GAE that basically checked your Python code, live. This made +it fun and interesting. Their application can be seen at +http://singpath.com + +I wanted an implementation that was not tied to GAE and decided to write +one myself and the result is the "exam" app. The idea being that I can +use this to test students programming skills and not have to worry about +grading their answers myself and I can do so on my machines. + +You can define fairly complicated programming problems and have users +solve the problem and the solution is checked immediately. The system +supports pretty much arbitrary Python and uses "test cases" to test the +implementations of the students. It also supports simple bash scripts +-- see the sample questions in "docs/". In addition it supports simple +multiple choice questions. Since it runs on your Python, you could +technically test any Python based library. It is distributed under the +BSD license. + +It can use a lot more work but the basics work and the app scales to +over 500+ simultaneous users. :) + +Dependencies +============= + +Before you install/deploy, make sure you have the following installed: + + - Django 1.3 or above. + - South (tested with 0.7.3). + +That and a running Python is pretty much all you need. Of course, for +serious deployment you are going to need Apache or some other decent +webserver. + + +Installation and Deployment +============================= + +To install/deploy this app follow the steps below: + + 1. Clone this repository and cd to the cloned repo. + + 2. Run:: + + $ python manage.py syncdb + [ enter password etc.] + + $ python manage.py migrate exam + + 3. Add questions by editing the "docs/sample_questions.py" or any other + file in the same format and then run the following:: + + $ python manage.py load_exam docs/sample_questions.py + + Note that you can supply multiple Python files as arguments and all of + those will be added to the database. + + 4. First run the python server provided. This ensures that the code is + executed in a safe environment. Do this like so:: + + $ sudo python code_server.py + + Put this in the background once it has started since this will not + return back the prompt. It is important that the server be running + *before* students start attempting the exam. Using sudo is + necessary since the server is run as the user "nobody". This runs + on the ports configured in the settings.py file in the variable + "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. + This is the maximum time allowed to execute the submitted code. + Note that this will likely spawn multiple processes as "nobody" + depending on the number of server ports specified. + + 5. Now, run:: + + $ python manage.py runserver : + + For deployment use Apache or a real webserver, see below for more + information. + + 6. Go to http://deserved_host_or_ip:desired_port/admin + + 7. Login with your credentials and look at the questions and modify if + needed. Create a new Quiz, set the date and duration or + activate/deactivate the quiz. + + 8. Now ask users to login at: + + http://host:port/exam + + And you should be all set. + + 9. Note that the directory "output" will contain directories, one for each + user. Users can potentially write output into these that can be used + for checking later. + + 10. As admin user you can visit http://host/exam/monitor to view + results and user data interactively. You could also "grade" the + papers manually if needed. + + 11. You may dump the results and user data using the results2csv and + dump_user_data commands. + +WARNING: django is running in debug mode for this currently, CHANGE it +during deployment. To do this, edit settings.py and set DEBUG to False. +Also look at other settings and change them suitably. + +The file docs/sample_questions.py is a template that you can use for your +own questions. + +Additional commands available +============================== + +We provide several convenient commands for you to use: + + - load_exam : load questions and a quiz from a python file. See + docs/sample_questions.py + + - load_questions_xml : load questions from XML file, see + docs/sample_questions.xml use of this is deprecated in favor of + load_exam. + + - results2csv : Dump the quiz results into a CSV file for further + processing. + + - dump_user_data : Dump out relevalt user data for either all users or + specified users. + +For more information on these do this:: + + $ ./manage.py help [command] + +where [command] is one of the above. + +Deploying via Apache +===================== + +For any serious deployment, you will need to deploy the app using a real +webserver like Apache. The ``apache/django.wsgi`` script should make it +easy to deploy this using mod_wsgi. You will need to add a line of the +form: + + WSGIScriptAlias / "/var/www/online_test/apache/django.wsgi" + +to your apache.conf. For more details see the Django docs here: + +https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ + + +Sometimes you might be in the situation where you are not hosted as +"host.org/exam/" but as "host.org/foo/exam/" for whatever reason. In +this case edit "settings.py" and set the "URL_ROOT" to the root you +have to serve at. In the above example for "host.org/foo/exam" set +URL_ROOT='/foo'. + +License +======= + +This is distributed under the terms of the BSD license. Copyright +information is at the bottom of this file. + +Authors +======= + +Main author: Prabhu Ramachandran + +I gratefully acknowledge help from the following: + + - Nishanth Amuluru originally from FOSSEE who wrote bulk of the + login/registration code. He wrote an initial first cut of a quiz app + which supported only simple questions which provided motivation for + this app. The current codebase does not share too much from his + implementation although there are plenty of similarities. + + - Harish Badrinath (FOSSEE) -- who provided a first cut of the bash + related scripts. + + - Srikant Patnaik and Thomas Stephen Lee, who helped deploy and test + the code. + + +Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) + diff --git a/testapp/__init__.py b/testapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testapp/apache/django.wsgi b/testapp/apache/django.wsgi new file mode 100644 index 0000000..ef88526 --- /dev/null +++ b/testapp/apache/django.wsgi @@ -0,0 +1,18 @@ +import os +from os.path import dirname, abspath +import sys + +# This file is inside online_test/apache/django.wsgi +# pth should be online_test +pth = abspath(dirname(dirname(__file__))) +if pth not in sys.path: + sys.path.append(pth) +# Now add the parent of online_test also. +pth = dirname(pth) +if pth not in sys.path: + sys.path.append(pth) + +os.environ['DJANGO_SETTINGS_MODULE'] = 'online_test.settings' + +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() diff --git a/testapp/code_server.py b/testapp/code_server.py new file mode 100755 index 0000000..1276c76 --- /dev/null +++ b/testapp/code_server.py @@ -0,0 +1,360 @@ +#!/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:: + + $ 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. +""" +import sys +import traceback +from SimpleXMLRPCServer import SimpleXMLRPCServer +import pwd +import os +import stat +from os.path import isdir, dirname, abspath, join, isfile +import signal +from multiprocessing import Process, Queue +import subprocess + +# Local imports. +from settings import SERVER_PORTS, SERVER_TIMEOUT, SERVER_POOL_PORT + +MY_DIR = abspath(dirname(__file__)) + +def run_as_nobody(): + """Runs the current process as nobody.""" + # Set the effective uid and to that of nobody. + nobody = pwd.getpwnam('nobody') + os.setegid(nobody.pw_gid) + os.seteuid(nobody.pw_uid) + + +# Raised when the code times-out. +# c.f. http://pguides.net/python/timeout-a-function +class TimeoutException(Exception): + pass + +def timeout_handler(signum, frame): + """A handler for the ALARM signal.""" + raise TimeoutException('Code took too long to run.') + + +################################################################################ +# `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 __init__(self, port, queue): + self.port = port + self.queue = queue + msg = 'Code took more than %s seconds to run. You probably '\ + 'have an infinite loop in your code.'%SERVER_TIMEOUT + self.timeout_msg = msg + + def run_python_code(self, answer, test_code, in_dir=None): + """Tests given Python function (`answer`) with the `test_code` supplied. + If the optional `in_dir` keyword argument is supplied it changes the + directory to that directory (it does not change it back to the original when + done). This function also timesout when the function takes more than + SERVER_TIMEOUT seconds to run to prevent runaway code. + + Returns + ------- + + A tuple: (success, error message). + + """ + if in_dir is not None and isdir(in_dir): + os.chdir(in_dir) + + # Add a new signal handler for the execution of this code. + old_handler = signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(SERVER_TIMEOUT) + + success = False + tb = None + try: + submitted = compile(answer, '', mode='exec') + g = {} + exec submitted in g + _tests = compile(test_code, '', mode='exec') + exec _tests in g + except TimeoutException: + err = self.timeout_msg + except AssertionError: + type, value, tb = sys.exc_info() + info = traceback.extract_tb(tb) + fname, lineno, func, text = info[-1] + text = str(test_code).splitlines()[lineno-1] + err = "{0} {1} in: {2}".format(type.__name__, str(value), text) + except: + type, value = sys.exc_info()[:2] + err = "Error: {0}".format(repr(value)) + else: + success = True + err = 'Correct answer' + finally: + del tb + # Set back any original signal handler. + signal.signal(signal.SIGALRM, old_handler) + + # Cancel the signal if any, see signal.alarm documentation. + signal.alarm(0) + + # Put us back into the server pool queue since we are free now. + self.queue.put(self.port) + + return success, err + + def run_bash_code(self, answer, test_code, in_dir=None): + """Tests given Bash code (`answer`) with the `test_code` supplied. + + The testcode should typically contain two lines, the first is a path to + the reference script we are to compare against. The second is a path + to the arguments to be supplied to the reference and submitted script. + The output of these will be compared for correctness. + + If the path's start with a "/" then we assume they are absolute paths. + If not, we assume they are relative paths w.r.t. the location of this + code_server script. + + If the optional `in_dir` keyword argument is supplied it changes the + directory to that directory (it does not change it back to the original when + done). + + Returns + ------- + + A tuple: (success, error message). + + """ + if in_dir is not None and isdir(in_dir): + os.chdir(in_dir) + + def _set_exec(fname): + os.chmod(fname, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR + |stat.S_IRGRP|stat.S_IWGRP|stat.S_IXGRP + |stat.S_IROTH|stat.S_IWOTH|stat.S_IXOTH) + submit_f = open('submit.sh', 'w') + submit_f.write(answer.lstrip()); submit_f.close() + submit_path = abspath(submit_f.name) + _set_exec(submit_path) + + ref_path, test_case_path = test_code.strip().splitlines() + if not ref_path.startswith('/'): + ref_path = join(MY_DIR, ref_path) + if not test_case_path.startswith('/'): + test_case_path = join(MY_DIR, test_case_path) + + # Add a new signal handler for the execution of this code. + old_handler = signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(SERVER_TIMEOUT) + + # Do whatever testing needed. + success = False + try: + success, err = self.check_bash_script(ref_path, submit_path, test_case_path) + except TimeoutException: + err = self.timeout_msg + except: + type, value = sys.exc_info()[:2] + err = "Error: {0}".format(repr(value)) + finally: + # Set back any original signal handler. + signal.signal(signal.SIGALRM, old_handler) + + # Delete the created file. + os.remove(submit_path) + + # Cancel the signal if any, see signal.alarm documentation. + signal.alarm(0) + + # Put us back into the server pool queue since we are free now. + self.queue.put(self.port) + + return success, err + + def _run_command(self, cmd_args, *args, **kw): + """Run a command in a subprocess while blocking, the process is killed + if it takes more than 2 seconds to run. Return the Popen object, the + stdout and stderr. + """ + try: + proc = subprocess.Popen(cmd_args, *args, **kw) + stdout, stderr = proc.communicate() + except TimeoutException: + # Runaway code, so kill it. + proc.kill() + # Re-raise exception. + raise + return proc, stdout, stderr + + def check_bash_script(self, ref_script_path, submit_script_path, + test_case_path=None): + """ Function validates student script using instructor script as + reference. Test cases can optionally be provided. The first argument + ref_script_path, is the path to instructor script, it is assumed to + have executable permission. The second argument submit_script_path, is + the path to the student script, it is assumed to have executable + permission. The Third optional argument is the path to test the + scripts. Each line in this file is a test case and each test case is + passed to the script as standard arguments. + + Returns + -------- + + returns (True, "Correct answer") : If the student script passes all test + cases/have same output, when compared to the instructor script + + returns (False, error_msg): If + the student script fails a single test/have dissimilar output, when + compared to the instructor script. + + Returns (False, error_msg): If mandatory arguments are not files or if the + required permissions are not given to the file(s). + + """ + if not isfile(ref_script_path): + return False, "No file at %s"%ref_script_path + if not isfile(submit_script_path): + return False, 'No file at %s'%submit_script_path + if not os.access(ref_script_path, os.X_OK): + return False, 'Script %s is not executable'%ref_script_path + if not os.access(submit_script_path, os.X_OK): + return False, 'Script %s is not executable'%submit_script_path + + if test_case_path is None: + ret = self._run_command(ref_script_path, stdin=None, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc, inst_stdout, inst_stderr = ret + ret = self._run_command(submit_script_path, stdin=None, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc, stdnt_stdout, stdnt_stderr = ret + if inst_stdout == stdnt_stdout: + return True, 'Correct answer' + else: + err = "Error: expected %s, got %s"%(inst_stderr, stdnt_stderr) + return False, err + else: + if not isfile(test_case_path): + return False, "No test case at %s"%test_case_path + if not os.access(ref_script_path, os.R_OK): + return False, "Test script %s, not readable"%test_case_path + valid_answer = True # We initially make it one, so that we can stop + # once a test case fails + loop_count = 0 # Loop count has to be greater than or equal to one. + # Useful for caching things like empty test files,etc. + test_cases = open(test_case_path).readlines() + num_lines = len(test_cases) + for test_case in test_cases: + loop_count += 1 + if valid_answer: + args = [ ref_script_path ] + [x for x in test_case.split()] + ret = self._run_command(args, stdin=None, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc, inst_stdout, inst_stderr = ret + args = [ submit_script_path ] + [x for x in test_case.split()] + ret = self._run_command(args, stdin=None, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc, stdnt_stdout, stdnt_stderr = ret + valid_answer = inst_stdout == stdnt_stdout + if valid_answer and (num_lines == loop_count): + return True, "Correct answer" + else: + err = "Error: expected %s, got %s"%(inst_stdout+inst_stderr, + stdnt_stdout+stdnt_stderr) + return False, err + + + def run(self): + """Run XMLRPC server, serving our methods. + """ + server = SimpleXMLRPCServer(("localhost", self.port)) + self.server = server + server.register_instance(self) + self.queue.put(self.port) + server.serve_forever() + + +################################################################################ +# `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. + + Parameters + ---------- + + ports : list(int) + List of ports at which the CodeServer's should run. + + pool_port : int + Port at which the server pool should serve. + """ + self.my_port = pool_port + self.ports = ports + queue = Queue(maxsize=len(ports)) + self.queue = queue + servers = [] + for port in ports: + server = CodeServer(port, queue) + servers.append(server) + p = Process(target=server.run) + p.start() + self.servers = servers + + def get_server_port(self): + """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 + + def run(self): + """Run server which returns an available server port where code + can be executed. + """ + server = SimpleXMLRPCServer(("localhost", self.my_port)) + self.server = server + server.register_instance(self) + server.serve_forever() + + +################################################################################ +def main(): + run_as_nobody() + if len(sys.argv) == 1: + ports = SERVER_PORTS + else: + ports = [int(x) for x in sys.argv[1:]] + + server_pool = ServerPool(ports=ports, pool_port=SERVER_POOL_PORT) + server_pool.run() + +if __name__ == '__main__': + main() diff --git a/testapp/docs/sample.args b/testapp/docs/sample.args new file mode 100644 index 0000000..4d9f00d --- /dev/null +++ b/testapp/docs/sample.args @@ -0,0 +1,2 @@ +1 2 +2 1 diff --git a/testapp/docs/sample.sh b/testapp/docs/sample.sh new file mode 100755 index 0000000..e935cb3 --- /dev/null +++ b/testapp/docs/sample.sh @@ -0,0 +1,2 @@ +#!/bin/bash +[[ $# -eq 2 ]] && echo $(( $1 + $2 )) && exit $(( $1 + $2 )) diff --git a/testapp/docs/sample_questions.py b/testapp/docs/sample_questions.py new file mode 100644 index 0000000..aa7f239 --- /dev/null +++ b/testapp/docs/sample_questions.py @@ -0,0 +1,59 @@ +from datetime import date + +questions = [ +Question( + summary='Factorial', + points=2, + type="python", + description=''' +Write a function called fact which takes a single integer argument +(say n) and returns the factorial of the number. +For example:
+fact(3) -> 6 +''', + test=''' +assert fact(0) == 1 +assert fact(5) == 120 +'''), + +Question( + summary='Simple function', + points=1, + type="python", + description='''Create a simple function called sqr which takes a single +argument and returns the square of the argument. For example:
+sqr(3) -> 9.''', + test=''' +import math +assert sqr(3) == 9 +assert abs(sqr(math.sqrt(2)) - 2.0) < 1e-14 + '''), +Question( + summary='Bash addition', + points=2, + type="bash", + description='''Write a shell script which takes two arguments on the + command line and prints the sum of the two on the output.''', + test='''\ +docs/sample.sh +docs/sample.args +'''), +Question( + summary='Size of integer in Python', + points=0.5, + type="mcq", + description='''What is the largest integer value that can be represented +in Python?''', + options='''No Limit +2**32 +2**32 - 1 +None of the above +''', + test = "No Limit" + ), +] + +quiz = Quiz(start_date=date.today(), + duration=10, + description='Basic Python Quiz 1' + ) diff --git a/testapp/docs/sample_questions.xml b/testapp/docs/sample_questions.xml new file mode 100644 index 0000000..53c76f8 --- /dev/null +++ b/testapp/docs/sample_questions.xml @@ -0,0 +1,43 @@ + + + + +Factorial + + +Write a function called "fact" which takes a single integer argument (say "n") +and returns the factorial of the number. +For example fact(3) -> 6 + +2 +python + +assert fact(0) == 1 +assert fact(5) == 120 + + + + + + + +Simple function + + +Create a simple function called "sqr" which takes a single argument and +returns the square of the argument +For example sqr(3) -> 9. + +1 +python + +import math +assert sqr(3) == 9 +assert abs(sqr(math.sqrt(2)) - 2.0) < 1e-14 + + + + + + + diff --git a/testapp/exam/__init__.py b/testapp/exam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testapp/exam/admin.py b/testapp/exam/admin.py new file mode 100644 index 0000000..8482ef9 --- /dev/null +++ b/testapp/exam/admin.py @@ -0,0 +1,5 @@ +from exam.models import Question, Quiz +from django.contrib import admin + +admin.site.register(Question) +admin.site.register(Quiz) diff --git a/testapp/exam/forms.py b/testapp/exam/forms.py new file mode 100644 index 0000000..a5ca26f --- /dev/null +++ b/testapp/exam/forms.py @@ -0,0 +1,95 @@ +from django import forms +from exam.models import Profile + +from django.contrib.auth import authenticate +from django.contrib.auth.models import User + +from string import letters, punctuation, digits + +UNAME_CHARS = letters + "._" + digits +PWD_CHARS = letters + punctuation + digits + +class UserRegisterForm(forms.Form): + + username = forms.CharField(max_length=30, + help_text='Letters, digits, period and underscores only.') + email = forms.EmailField() + password = forms.CharField(max_length=30, + widget=forms.PasswordInput()) + confirm_password = forms.CharField(max_length=30, + widget=forms.PasswordInput()) + first_name = forms.CharField(max_length=30) + last_name = forms.CharField(max_length=30) + roll_number = forms.CharField(max_length=30, + help_text="Use a dummy if you don't have one.") + institute = forms.CharField(max_length=128, + help_text='Institute/Organization') + department = forms.CharField(max_length=64, + help_text='Department you work/study at') + position = forms.CharField(max_length=64, + help_text='Student/Faculty/Researcher/Industry/etc.') + + def clean_username(self): + u_name = self.cleaned_data["username"] + + if u_name.strip(UNAME_CHARS): + msg = "Only letters, digits, period and underscore characters are "\ + "allowed in username" + raise forms.ValidationError(msg) + + try: + User.objects.get(username__exact = u_name) + raise forms.ValidationError("Username already exists.") + except User.DoesNotExist: + return u_name + + def clean_password(self): + pwd = self.cleaned_data['password'] + if pwd.strip(PWD_CHARS): + raise forms.ValidationError("Only letters, digits and punctuation are \ + allowed in password") + return pwd + + def clean_confirm_password(self): + c_pwd = self.cleaned_data['confirm_password'] + pwd = self.data['password'] + if c_pwd != pwd: + raise forms.ValidationError("Passwords do not match") + + return c_pwd + + def save(self): + u_name = self.cleaned_data["username"] + u_name = u_name.lower() + pwd = self.cleaned_data["password"] + email = self.cleaned_data['email'] + new_user = User.objects.create_user(u_name, email, pwd) + + new_user.first_name = self.cleaned_data["first_name"] + new_user.last_name = self.cleaned_data["last_name"] + new_user.save() + + cleaned_data = self.cleaned_data + new_profile = Profile(user=new_user) + new_profile.roll_number = cleaned_data["roll_number"] + new_profile.institute = cleaned_data["institute"] + new_profile.department = cleaned_data["department"] + new_profile.position = cleaned_data["position"] + new_profile.save() + + return u_name, pwd + +class UserLoginForm(forms.Form): + username = forms.CharField(max_length = 30) + password = forms.CharField(max_length=30, widget=forms.PasswordInput()) + + def clean(self): + super(UserLoginForm, self).clean() + u_name, pwd = self.cleaned_data["username"], self.cleaned_data["password"] + user = authenticate(username = u_name, password = pwd) + + if not user: + raise forms.ValidationError("Invalid username/password") + + return user + diff --git a/testapp/exam/management/__init__.py b/testapp/exam/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testapp/exam/management/commands/__init__.py b/testapp/exam/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testapp/exam/management/commands/dump_user_data.py b/testapp/exam/management/commands/dump_user_data.py new file mode 100644 index 0000000..ec016bb --- /dev/null +++ b/testapp/exam/management/commands/dump_user_data.py @@ -0,0 +1,98 @@ +import sys + +# Django imports. +from django.core.management.base import BaseCommand +from django.template import Template, Context + +# Local imports. +from exam.views import get_user_data +from exam.models import User + +data_template = Template('''\ +=============================================================================== +Data for {{ data.user.get_full_name.title }} ({{ data.user.username }}) + +Name: {{ data.user.get_full_name.title }} +Username: {{ data.user.username }} +{% if data.profile %}\ +Roll number: {{ data.profile.roll_number }} +Position: {{ data.profile.position }} +Department: {{ data.profile.department }} +Institute: {{ data.profile.institute }} +{% endif %}\ +Email: {{ data.user.email }} +Date joined: {{ data.user.date_joined }} +Last login: {{ data.user.last_login }} +{% for paper in data.papers %} +Paper: {{ paper.quiz.description }} +--------------------------------------- +Marks obtained: {{ paper.get_total_marks }} +Questions correctly answered: {{ paper.get_answered_str }} +Total attempts at questions: {{ paper.answers.count }} +Start time: {{ paper.start_time }} +User IP address: {{ paper.user_ip }} +{% if paper.answers.count %} +Answers +------- +{% for question, answers in paper.get_question_answers.items %} +Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }}) +{% if question.type == "mcq" %}\ +############################################################################### +Choices: {% for option in question.options.strip.splitlines %} {{option}}, {% endfor %} +Student answer: {{ answers.0|safe }} +{% else %}{# non-mcq questions #}\ +{% for answer in answers %}\ +############################################################################### +{{ answer.answer.strip|safe }} +# Autocheck: {{ answer.error|safe }} +{% endfor %}{# for answer in answers #}\ +{% endif %}\ +{% with answers|last as answer %}\ +Marks: {{answer.marks}} +{% endwith %}\ +{% endfor %}{# for question, answers ... #}\ + +Teacher comments +----------------- +{{ paper.comments|default:"None" }} +{% endif %}{# if paper.answers.count #}\ +{% endfor %}{# for paper in data.papers #} +''') + + +def dump_user_data(unames, stdout): + '''Dump user data given usernames (a sequence) if none is given dump all + their data. The data is dumped to stdout. + ''' + if not unames: + try: + users = User.objects.all() + except User.DoesNotExist: + pass + else: + users = [] + for uname in unames: + try: + user = User.objects.get(username__exact = uname) + except User.DoesNotExist: + stdout.write('User %s does not exist'%uname) + else: + users.append(user) + + for user in users: + data = get_user_data(user.username) + context = Context({'data': data}) + result = data_template.render(context) + stdout.write(result.encode('ascii', 'xmlcharrefreplace')) + +class Command(BaseCommand): + args = ' ... ' + help = '''Dumps all user data to stdout, optional usernames can be + specified. If none is specified all user data is dumped. + ''' + + def handle(self, *args, **options): + """Handle the command.""" + # Dump data. + dump_user_data(args, self.stdout) + diff --git a/testapp/exam/management/commands/load_exam.py b/testapp/exam/management/commands/load_exam.py new file mode 100644 index 0000000..3f247a1 --- /dev/null +++ b/testapp/exam/management/commands/load_exam.py @@ -0,0 +1,55 @@ +# System library imports. +from os.path import basename + +# Django imports. +from django.core.management.base import BaseCommand + +# Local imports. +from exam.models import Question, Quiz + +def clear_exam(): + """Deactivate all questions from the database.""" + for question in Question.objects.all(): + question.active = False + question.save() + + # Deactivate old quizzes. + for quiz in Quiz.objects.all(): + quiz.active = False + quiz.save() + +def load_exam(filename): + """Load questions and quiz from the given Python file. The Python file + should declare a list of name "questions" which define all the questions + in pure Python. It can optionally load a Quiz from an optional 'quiz' + object. + """ + # Simply exec the given file and we are done. + exec(open(filename).read()) + + if 'questions' not in locals(): + msg = 'No variable named "questions" with the Questions in file.' + raise NameError(msg) + + for question in questions: + question.save() + + if 'quiz' in locals(): + quiz.save() + +class Command(BaseCommand): + args = '' + help = '''loads the questions from given Python files which declare the + questions in a list called "questions".''' + + def handle(self, *args, **options): + """Handle the command.""" + # Delete existing stuff. + clear_exam() + + # Load from files. + for fname in args: + self.stdout.write('Importing from {0} ... '.format(basename(fname))) + load_exam(fname) + self.stdout.write('Done\n') + diff --git a/testapp/exam/management/commands/load_questions_xml.py b/testapp/exam/management/commands/load_questions_xml.py new file mode 100644 index 0000000..8bc2701 --- /dev/null +++ b/testapp/exam/management/commands/load_questions_xml.py @@ -0,0 +1,73 @@ +# System library imports. +from os.path import basename +from xml.dom.minidom import parse +from htmlentitydefs import name2codepoint +import re + +# Django imports. +from django.core.management.base import BaseCommand + +# Local imports. +from exam.models import Question + +def decode_html(html_str): + """Un-escape or decode HTML strings to more usable Python strings. + From here: http://wiki.python.org/moin/EscapingHtml + """ + return re.sub('&(%s);' % '|'.join(name2codepoint), + lambda m: unichr(name2codepoint[m.group(1)]), html_str) + +def clear_questions(): + """Deactivate all questions from the database.""" + for question in Question.objects.all(): + question.active = False + question.save() + +def load_questions_xml(filename): + """Load questions from the given XML file.""" + q_bank = parse(filename).getElementsByTagName("question") + + for question in q_bank: + + summary_node = question.getElementsByTagName("summary")[0] + summary = (summary_node.childNodes[0].data).strip() + + desc_node = question.getElementsByTagName("description")[0] + description = (desc_node.childNodes[0].data).strip() + + type_node = question.getElementsByTagName("type")[0] + type = (type_node.childNodes[0].data).strip() + + points_node = question.getElementsByTagName("points")[0] + points = float((points_node.childNodes[0].data).strip()) \ + if points_node else 1.0 + + test_node = question.getElementsByTagName("test")[0] + test = decode_html((test_node.childNodes[0].data).strip()) + + opt_node = question.getElementsByTagName("options")[0] + opt = decode_html((opt_node.childNodes[0].data).strip()) + + new_question = Question(summary=summary, + description=description, + points=points, + options=opt, + type=type, + test=test) + new_question.save() + +class Command(BaseCommand): + args = '' + help = 'loads the questions from given XML files' + + def handle(self, *args, **options): + """Handle the command.""" + # Delete existing stuff. + clear_questions() + + # Load from files. + for fname in args: + self.stdout.write('Importing from {0} ... '.format(basename(fname))) + load_questions_xml(fname) + self.stdout.write('Done\n') + diff --git a/testapp/exam/management/commands/results2csv.py b/testapp/exam/management/commands/results2csv.py new file mode 100644 index 0000000..2993745 --- /dev/null +++ b/testapp/exam/management/commands/results2csv.py @@ -0,0 +1,69 @@ +# System library imports. +import sys +from os.path import basename + +# Django imports. +from django.core.management.base import BaseCommand +from django.template import Template, Context + +# Local imports. +from exam.models import Quiz, QuestionPaper + +result_template = Template('''\ +"name","username","rollno","email","answered","total","attempts","position",\ +"department","institute" +{% for paper in papers %}\ +"{{ paper.user.get_full_name.title }}",\ +"{{ paper.user.username }}",\ +"{{ paper.profile.roll_number }}",\ +"{{ paper.user.email }}",\ +"{{ paper.get_answered_str }}",\ +{{ paper.get_total_marks }},\ +{{ paper.answers.count }},\ +"{{ paper.profile.position }}",\ +"{{ paper.profile.department }}",\ +"{{ paper.profile.institute }}" +{% endfor %}\ +''') + +def results2csv(filename, stdout): + """Write exam data to a CSV file. It prompts the user to choose the + appropriate quiz. + """ + qs = Quiz.objects.all() + + if len(qs) > 1: + print "Select quiz to save:" + for q in qs: + stdout.write('%d. %s\n'%(q.id, q.description)) + quiz_id = int(raw_input("Please select quiz: ")) + try: + quiz = Quiz.objects.get(id=quiz_id) + except Quiz.DoesNotExist: + stdout.write("Sorry, quiz %d does not exist!\n"%quiz_id) + sys.exit(1) + else: + quiz = qs[0] + + papers = QuestionPaper.objects.filter(quiz=quiz, + user__profile__isnull=False) + stdout.write("Saving results of %s to %s ... "%(quiz.description, + basename(filename))) + # Render the data and write it out. + f = open(filename, 'w') + context = Context({'papers': papers}) + f.write(result_template.render(context)) + f.close() + + stdout.write('Done\n') + +class Command(BaseCommand): + args = '' + help = '''Writes out the results of a quiz to a CSV file. Prompt user + to select appropriate quiz if there are multiple. + ''' + + def handle(self, *args, **options): + """Handle the command.""" + # Save to file. + results2csv(args[0], self.stdout) diff --git a/testapp/exam/migrations/0001_initial.py b/testapp/exam/migrations/0001_initial.py new file mode 100644 index 0000000..49048cc --- /dev/null +++ b/testapp/exam/migrations/0001_initial.py @@ -0,0 +1,193 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Profile' + db.create_table('exam_profile', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['auth.User'], unique=True)), + ('roll_number', self.gf('django.db.models.fields.CharField')(max_length=20)), + ('institute', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('department', self.gf('django.db.models.fields.CharField')(max_length=64)), + ('position', self.gf('django.db.models.fields.CharField')(max_length=64)), + )) + db.send_create_signal('exam', ['Profile']) + + # Adding model 'Question' + db.create_table('exam_question', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('summary', self.gf('django.db.models.fields.CharField')(max_length=256)), + ('description', self.gf('django.db.models.fields.TextField')()), + ('points', self.gf('django.db.models.fields.FloatField')(default=1.0)), + ('test', self.gf('django.db.models.fields.TextField')(blank=True)), + ('options', self.gf('django.db.models.fields.TextField')(blank=True)), + ('type', self.gf('django.db.models.fields.CharField')(max_length=24)), + ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), + )) + db.send_create_signal('exam', ['Question']) + + # Adding model 'Answer' + db.create_table('exam_answer', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['exam.Question'])), + ('answer', self.gf('django.db.models.fields.TextField')()), + ('error', self.gf('django.db.models.fields.TextField')()), + ('marks', self.gf('django.db.models.fields.FloatField')(default=0.0)), + ('correct', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('exam', ['Answer']) + + # Adding model 'Quiz' + db.create_table('exam_quiz', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('start_date', self.gf('django.db.models.fields.DateField')()), + ('duration', self.gf('django.db.models.fields.IntegerField')(default=20)), + ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('description', self.gf('django.db.models.fields.CharField')(max_length=256)), + )) + db.send_create_signal('exam', ['Quiz']) + + # Adding model 'QuestionPaper' + db.create_table('exam_questionpaper', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('profile', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['exam.Profile'])), + ('quiz', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['exam.Quiz'])), + ('start_time', self.gf('django.db.models.fields.DateTimeField')()), + ('user_ip', self.gf('django.db.models.fields.CharField')(max_length=15)), + ('key', self.gf('django.db.models.fields.CharField')(max_length=10)), + ('active', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('questions', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('questions_answered', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('comments', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('exam', ['QuestionPaper']) + + # Adding M2M table for field answers on 'QuestionPaper' + db.create_table('exam_questionpaper_answers', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('questionpaper', models.ForeignKey(orm['exam.questionpaper'], null=False)), + ('answer', models.ForeignKey(orm['exam.answer'], null=False)) + )) + db.create_unique('exam_questionpaper_answers', ['questionpaper_id', 'answer_id']) + + + def backwards(self, orm): + + # Deleting model 'Profile' + db.delete_table('exam_profile') + + # Deleting model 'Question' + db.delete_table('exam_question') + + # Deleting model 'Answer' + db.delete_table('exam_answer') + + # Deleting model 'Quiz' + db.delete_table('exam_quiz') + + # Deleting model 'QuestionPaper' + db.delete_table('exam_questionpaper') + + # Removing M2M table for field answers on 'QuestionPaper' + db.delete_table('exam_questionpaper_answers') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'exam.answer': { + 'Meta': {'object_name': 'Answer'}, + 'answer': ('django.db.models.fields.TextField', [], {}), + 'correct': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'error': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'marks': ('django.db.models.fields.FloatField', [], {'default': '0.0'}), + 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['exam.Question']"}) + }, + 'exam.profile': { + 'Meta': {'object_name': 'Profile'}, + 'department': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'institute': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'position': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'roll_number': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'exam.question': { + 'Meta': {'object_name': 'Question'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'options': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'points': ('django.db.models.fields.FloatField', [], {'default': '1.0'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'test': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '24'}) + }, + 'exam.questionpaper': { + 'Meta': {'object_name': 'QuestionPaper'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'answers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['exam.Answer']", 'symmetrical': 'False'}), + 'comments': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '10'}), + 'profile': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['exam.Profile']"}), + 'questions': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'questions_answered': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'quiz': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['exam.Quiz']"}), + 'start_time': ('django.db.models.fields.DateTimeField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'user_ip': ('django.db.models.fields.CharField', [], {'max_length': '15'}) + }, + 'exam.quiz': { + 'Meta': {'object_name': 'Quiz'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'description': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'duration': ('django.db.models.fields.IntegerField', [], {'default': '20'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'start_date': ('django.db.models.fields.DateField', [], {}) + } + } + + complete_apps = ['exam'] diff --git a/testapp/exam/migrations/__init__.py b/testapp/exam/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/testapp/exam/models.py b/testapp/exam/models.py new file mode 100644 index 0000000..717e02e --- /dev/null +++ b/testapp/exam/models.py @@ -0,0 +1,221 @@ +import datetime +from django.db import models +from django.contrib.auth.models import User + +################################################################################ +class Profile(models.Model): + """Profile for a user to store roll number and other details.""" + user = models.OneToOneField(User) + roll_number = models.CharField(max_length=20) + institute = models.CharField(max_length=128) + department = models.CharField(max_length=64) + position = models.CharField(max_length=64) + + +QUESTION_TYPE_CHOICES = ( + ("python", "Python"), + ("bash", "Bash"), + ("mcq", "MultipleChoice"), + ) + +################################################################################ +class Question(models.Model): + """A question in the database.""" + + # A one-line summary of the question. + summary = models.CharField(max_length=256) + + # The question text, should be valid HTML. + description = models.TextField() + + # Number of points for the question. + points = models.FloatField(default=1.0) + + # Test cases for the question in the form of code that is run. + test = models.TextField(blank=True) + + # Any multiple choice options. Place one option per line. + options = models.TextField(blank=True) + + # The type of question. + type = models.CharField(max_length=24, choices=QUESTION_TYPE_CHOICES) + + # Is this question active or not. If it is inactive it will not be used + # when creating a QuestionPaper. + active = models.BooleanField(default=True) + + def __unicode__(self): + return self.summary + + +################################################################################ +class Answer(models.Model): + """Answers submitted by users. + """ + # The question for which we are an answer. + question = models.ForeignKey(Question) + + # The answer submitted by the user. + answer = models.TextField() + + # Error message when auto-checking the answer. + error = models.TextField() + + # Marks obtained for the answer. This can be changed by the teacher if the + # grading is manual. + marks = models.FloatField(default=0.0) + + # Is the answer correct. + correct = models.BooleanField(default=False) + + def __unicode__(self): + return self.answer + +################################################################################ +class Quiz(models.Model): + """A quiz that students will participate in. One can think of this + as the "examination" event. + """ + + # The starting/ending date of the quiz. + start_date = models.DateField("Date of the quiz") + + # This is always in minutes. + duration = models.IntegerField("Duration of quiz in minutes", default=20) + + # Is the quiz active. The admin should deactivate the quiz once it is + # complete. + active = models.BooleanField(default=True) + + # Description of quiz. + description = models.CharField(max_length=256) + + class Meta: + verbose_name_plural = "Quizzes" + + def __unicode__(self): + desc = self.description or 'Quiz' + return '%s: on %s for %d minutes'%(desc, self.start_date, self.duration) + + +################################################################################ +class QuestionPaper(models.Model): + """A question paper for a student -- one per student typically. + """ + # The user taking this question paper. + user = models.ForeignKey(User) + + # The user's profile, we store a reference to make it easier to access the + # data. + profile = models.ForeignKey(Profile) + + # The Quiz to which this question paper is attached to. + quiz = models.ForeignKey(Quiz) + + # The time when this paper was started by the user. + start_time = models.DateTimeField() + + # User's IP which is logged. + user_ip = models.CharField(max_length=15) + # Unused currently. + key = models.CharField(max_length=10) + + # used to allow/stop a user from retaking the question paper. + active = models.BooleanField(default = True) + + # The questions (a list of ids separated by '|') + questions = models.CharField(max_length=128) + # The questions successfully answered (a list of ids separated by '|') + questions_answered = models.CharField(max_length=128) + + # All the submitted answers. + answers = models.ManyToManyField(Answer) + + # Teacher comments on the question paper. + comments = models.TextField() + + def current_question(self): + """Returns the current active question to display.""" + qs = self.questions.split('|') + if len(qs) > 0: + return qs[0] + else: + return '' + + def questions_left(self): + """Returns the number of questions left.""" + qs = self.questions + if len(qs) == 0: + return 0 + else: + return qs.count('|') + 1 + + def completed_question(self, question_id): + """Removes the question from the list of questions and returns + the next.""" + qa = self.questions_answered + if len(qa) > 0: + self.questions_answered = '|'.join([qa, str(question_id)]) + else: + self.questions_answered = str(question_id) + qs = self.questions.split('|') + qs.remove(unicode(question_id)) + self.questions = '|'.join(qs) + self.save() + if len(qs) == 0: + return '' + else: + return qs[0] + + def skip(self): + """Skip the current question and return the next available question.""" + qs = self.questions.split('|') + if len(qs) == 0: + return '' + else: + # Put head at the end. + head = qs.pop(0) + qs.append(head) + self.questions = '|'.join(qs) + self.save() + return qs[0] + + def time_left(self): + """Return the time remaining for the user in seconds.""" + dt = datetime.datetime.now() - self.start_time + try: + secs = dt.total_seconds() + except AttributeError: + # total_seconds is new in Python 2.7. :( + secs = dt.seconds + dt.days*24*3600 + total = self.quiz.duration*60.0 + remain = max(total - secs, 0) + return int(remain) + + def get_answered_str(self): + """Returns the answered questions, sorted and as a nice string.""" + qa = self.questions_answered.split('|') + answered = ', '.join(sorted(qa)) + return answered if answered else 'None' + + def get_total_marks(self): + """Returns the total marks earned by student for this paper.""" + return sum([x.marks for x in self.answers.filter(marks__gt=0.0)]) + + def get_question_answers(self): + """Return a dictionary with keys as questions and a list of the corresponding + answers. + """ + q_a = {} + for answer in self.answers.all(): + question = answer.question + if question in q_a: + q_a[question].append(answer) + else: + q_a[question] = [answer] + return q_a + + def __unicode__(self): + u = self.user + return u'Question paper for {0} {1}'.format(u.first_name, u.last_name) + diff --git a/testapp/exam/tests.py b/testapp/exam/tests.py new file mode 100644 index 0000000..501deb7 --- /dev/null +++ b/testapp/exam/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/testapp/exam/urls.py b/testapp/exam/urls.py new file mode 100644 index 0000000..34e329f --- /dev/null +++ b/testapp/exam/urls.py @@ -0,0 +1,16 @@ +from django.conf.urls.defaults import patterns, include, url + +urlpatterns = patterns('exam.views', + url(r'^$', 'index'), + url(r'^login/$', 'user_login'), + url(r'^register/$', 'user_register'), + url(r'^start/$', 'start'), + url(r'^quit/$', 'quit'), + url(r'^complete/$', 'complete'), + url(r'^monitor/$', 'monitor'), + url(r'^monitor/(?P\d+)/$', 'monitor'), + url(r'^user_data/(?P[a-zA-Z0-9_.]+)/$', 'user_data'), + url(r'^grade_user/(?P[a-zA-Z0-9_.]+)/$', 'grade_user'), + url(r'^(?P\d+)/$', 'question'), + url(r'^(?P\d+)/check/$', 'check'), +) diff --git a/testapp/exam/views.py b/testapp/exam/views.py new file mode 100644 index 0000000..c178a0b --- /dev/null +++ b/testapp/exam/views.py @@ -0,0 +1,351 @@ +import random +import string +import os +import stat +from os.path import dirname, pardir, abspath, join, exists +import datetime + +from django.contrib.auth import login, logout, authenticate +from django.shortcuts import render_to_response, get_object_or_404, redirect +from django.template import RequestContext +from django.http import Http404 +from django.db.models import Sum + +# Local imports. +from exam.models import Quiz, Question, QuestionPaper, Profile, Answer, User +from exam.forms import UserRegisterForm, UserLoginForm +from exam.xmlrpc_clients import code_server +from settings import URL_ROOT + +# The directory where user data can be saved. +OUTPUT_DIR = abspath(join(dirname(__file__), pardir, 'output')) + + +def my_redirect(url): + """An overridden redirect to deal with URL_ROOT-ing. See settings.py + for details.""" + return redirect(URL_ROOT + url) + +def my_render_to_response(template, context=None, **kwargs): + """Overridden render_to_response. + """ + if context is None: + context = {'URL_ROOT': URL_ROOT} + else: + context['URL_ROOT'] = URL_ROOT + return render_to_response(template, context, **kwargs) + + +def gen_key(no_of_chars): + """Generate a random key of the number of characters.""" + allowed_chars = string.digits+string.uppercase + return ''.join([random.choice(allowed_chars) for i in range(no_of_chars)]) + +def get_user_dir(user): + """Return the output directory for the user.""" + user_dir = join(OUTPUT_DIR, str(user.username)) + if not exists(user_dir): + os.mkdir(user_dir) + # Make it rwx by others. + os.chmod(user_dir, stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH \ + | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR \ + | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP) + return user_dir + +def index(request): + """The start page. + """ + user = request.user + if user.is_authenticated(): + return my_redirect("/exam/start/") + + return my_redirect("/exam/login/") + +def user_register(request): + """ Register a new user. + Create a user and corresponding profile and store roll_number also.""" + + user = request.user + if user.is_authenticated(): + return my_redirect("/exam/start/") + + if request.method == "POST": + form = UserRegisterForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + u_name, pwd = form.save() + + new_user = authenticate(username = u_name, password = pwd) + login(request, new_user) + return my_redirect("/exam/start/") + + else: + return my_render_to_response('exam/register.html', + {'form':form}, + context_instance=RequestContext(request)) + else: + form = UserRegisterForm() + return my_render_to_response('exam/register.html', + {'form':form}, + context_instance=RequestContext(request)) + +def user_login(request): + """Take the credentials of the user and log the user in.""" + + user = request.user + if user.is_authenticated(): + return my_redirect("/exam/start/") + + if request.method == "POST": + form = UserLoginForm(request.POST) + if form.is_valid(): + user = form.cleaned_data + login(request, user) + return my_redirect("/exam/start/") + else: + context = {"form": form} + return my_render_to_response('exam/login.html', context, + context_instance=RequestContext(request)) + else: + form = UserLoginForm() + context = {"form": form} + return my_render_to_response('exam/login.html', context, + context_instance=RequestContext(request)) + +def start(request): + user = request.user + try: + # Right now the app is designed so there is only one active quiz + # at a particular time. + quiz = Quiz.objects.get(active=True) + except Quiz.DoesNotExist: + msg = 'No active quiz found, please contact your '\ + 'instructor/administrator. Please login again thereafter.' + return complete(request, reason=msg) + try: + old_paper = QuestionPaper.objects.get(user=user, quiz=quiz) + q = old_paper.current_question() + return show_question(request, q) + except QuestionPaper.DoesNotExist: + ip = request.META['REMOTE_ADDR'] + key = gen_key(10) + try: + profile = user.get_profile() + except Profile.DoesNotExist: + msg = 'You do not have a profile and cannot take the quiz!' + raise Http404(msg) + + new_paper = QuestionPaper(user=user, user_ip=ip, key=key, + quiz=quiz, profile=profile) + new_paper.start_time = datetime.datetime.now() + + # Make user directory. + user_dir = get_user_dir(user) + + questions = [ str(_.id) for _ in Question.objects.filter(active=True) ] + random.shuffle(questions) + + new_paper.questions = "|".join(questions) + new_paper.save() + + # Show the user the intro page. + context = {'user': user} + ci = RequestContext(request) + return my_render_to_response('exam/intro.html', context, + context_instance=ci) + +def question(request, q_id): + user = request.user + if not user.is_authenticated(): + return my_redirect('/exam/login/') + q = get_object_or_404(Question, pk=q_id) + try: + paper = QuestionPaper.objects.get(user=request.user, quiz__active=True) + except QuestionPaper.DoesNotExist: + return my_redirect('/exam/start') + if not paper.quiz.active: + return complete(request, reason='The quiz has been deactivated!') + + time_left = paper.time_left() + if time_left == 0: + return complete(request, reason='Your time is up!') + quiz_name = paper.quiz.description + context = {'question': q, 'paper': paper, 'user': user, + 'quiz_name': quiz_name, + 'time_left': time_left} + ci = RequestContext(request) + return my_render_to_response('exam/question.html', context, + context_instance=ci) + +def show_question(request, q_id): + """Show a question if possible.""" + if len(q_id) == 0: + msg = 'Congratulations! You have successfully completed the quiz.' + return complete(request, msg) + else: + return question(request, q_id) + +def check(request, q_id): + user = request.user + if not user.is_authenticated(): + return my_redirect('/exam/login/') + question = get_object_or_404(Question, pk=q_id) + paper = QuestionPaper.objects.get(user=user, quiz__active=True) + answer = request.POST.get('answer') + skip = request.POST.get('skip', None) + + if skip is not None: + next_q = paper.skip() + return show_question(request, next_q) + + # Add the answer submitted, regardless of it being correct or not. + new_answer = Answer(question=question, answer=answer, correct=False) + new_answer.save() + 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 + # safely in a separate process (the code_server.py) running as nobody. + if question.type == 'mcq': + success = True # Only one attempt allowed for MCQ's. + if answer.strip() == question.test.strip(): + new_answer.correct = True + new_answer.marks = question.points + new_answer.error = 'Correct answer' + else: + new_answer.error = 'Incorrect answer' + else: + user_dir = get_user_dir(user) + success, err_msg = code_server.run_code(answer, question.test, + user_dir, question.type) + new_answer.error = err_msg + if success: + # Note the success and save it along with the marks. + new_answer.correct = success + new_answer.marks = question.points + + new_answer.save() + + if not success: # Should only happen for non-mcq questions. + time_left = paper.time_left() + if time_left == 0: + return complete(request, reason='Your time is up!') + if not paper.quiz.active: + return complete(request, reason='The quiz has been deactivated!') + + context = {'question': question, 'error_message': err_msg, + 'paper': paper, 'last_attempt': answer, + 'quiz_name': paper.quiz.description, + 'time_left': time_left} + ci = RequestContext(request) + + return my_render_to_response('exam/question.html', context, + context_instance=ci) + else: + next_q = paper.completed_question(question.id) + return show_question(request, next_q) + +def quit(request): + return my_render_to_response('exam/quit.html', + context_instance=RequestContext(request)) + +def complete(request, reason=None): + user = request.user + no = False + message = reason or 'The quiz has been completed. Thank you.' + if request.method == 'POST' and 'no' in request.POST: + no = request.POST.get('no', False) + if not no: + # Logout the user and quit with the message given. + logout(request) + context = {'message': message} + return my_render_to_response('exam/complete.html', context) + else: + return my_redirect('/exam/') + + +def monitor(request, quiz_id=None): + """Monitor the progress of the papers taken so far.""" + user = request.user + if not user.is_authenticated() and not user.is_staff: + raise Http404('You are not allowed to view this page!') + + if quiz_id is None: + quizzes = Quiz.objects.all() + context = {'papers': [], + 'quiz': None, + 'quizzes':quizzes} + return my_render_to_response('exam/monitor.html', context, + context_instance=RequestContext(request)) + # quiz_id is not None. + try: + quiz = Quiz.objects.get(id=quiz_id) + except Quiz.DoesNotExist: + papers = [] + quiz = None + else: + papers = QuestionPaper.objects.all().annotate( + total=Sum('answers__marks')).order_by('-total') + + context = {'papers': papers, 'quiz': quiz, 'quizzes': None} + return my_render_to_response('exam/monitor.html', context, + context_instance=RequestContext(request)) + +def get_user_data(username): + """For a given username, this returns a dictionary of important data + related to the user including all the user's answers submitted. + """ + user = User.objects.get(username=username) + papers = QuestionPaper.objects.filter(user=user) + + data = {} + try: + profile = user.get_profile() + except Profile.DoesNotExist: + # Admin user may have a paper by accident but no profile. + profile = None + data['user'] = user + data['profile'] = profile + data['papers'] = papers + return data + +def user_data(request, username): + """Render user data.""" + current_user = request.user + if not current_user.is_authenticated() and not current_user.is_staff: + raise Http404('You are not allowed to view this page!') + + data = get_user_data(username) + + context = {'data': data} + return my_render_to_response('exam/user_data.html', context, + context_instance=RequestContext(request)) + +def grade_user(request, username): + """Present an interface with which we can easily grade a user's papers + and update all their marks and also give comments for each paper. + """ + current_user = request.user + if not current_user.is_authenticated() and not current_user.is_staff: + raise Http404('You are not allowed to view this page!') + + data = get_user_data(username) + if request.method == 'POST': + papers = data['papers'] + for paper in papers: + for question, answers in paper.get_question_answers().iteritems(): + marks = float(request.POST.get('q%d_marks'%question.id)) + last_ans = answers[-1] + last_ans.marks = marks + last_ans.save() + paper.comments = request.POST.get('comments_%d'%paper.quiz.id) + paper.save() + + context = {'data': data} + return my_render_to_response('exam/user_data.html', context, + context_instance=RequestContext(request)) + else: + context = {'data': data} + return my_render_to_response('exam/grade_user.html', context, + context_instance=RequestContext(request)) + diff --git a/testapp/exam/xmlrpc_clients.py b/testapp/exam/xmlrpc_clients.py new file mode 100644 index 0000000..817e37d --- /dev/null +++ b/testapp/exam/xmlrpc_clients.py @@ -0,0 +1,78 @@ +from xmlrpclib import ServerProxy +import time +import random +import socket + +from settings import SERVER_PORTS, SERVER_POOL_PORT + + +class ConnectionError(Exception): + pass + +################################################################################ +# `CodeServerProxy` class. +################################################################################ +class CodeServerProxy(object): + """A class that manages accesing the farm of Python servers and making + calls to them such that no one XMLRPC server is overloaded. + """ + def __init__(self): + pool_url = 'http://localhost:%d'%(SERVER_POOL_PORT) + self.pool_server = ServerProxy(pool_url) + self.methods = {"python": 'run_python_code', + "bash": 'run_bash_code'} + + def run_code(self, answer, test_code, user_dir, language): + """Tests given code (`answer`) with the `test_code` supplied. If the + optional `in_dir` keyword argument is supplied it changes the directory + to that directory (it does not change it back to the original when + done). The parameter language specifies which language to use for the + tests. + + Parameters + ---------- + answer : str + The user's answer for the question. + test_code : str + The test code to check the user code with. + user_dir : str (directory) + The directory to run the tests inside. + language : str + The programming language to use. + + Returns + ------- + A tuple: (success, error message). + """ + method_name = self.methods[language] + try: + server = self._get_server() + method = getattr(server, method_name) + result = method(answer, test_code, user_dir) + except ConnectionError: + result = [False, 'Unable to connect to any code servers!'] + 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!") + proxy = ServerProxy('http://localhost:%d'%port) + return proxy + +# views.py calls this Python server which forwards the request to one +# of the running servers. +code_server = CodeServerProxy() + diff --git a/testapp/manage.py b/testapp/manage.py new file mode 100755 index 0000000..3e4eedc --- /dev/null +++ b/testapp/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +from django.core.management import execute_manager +import imp +try: + imp.find_module('settings') # Assumed to be in the same directory. +except ImportError: + import sys + sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) + sys.exit(1) + +import settings + +if __name__ == "__main__": + execute_manager(settings) diff --git a/testapp/output/README.txt b/testapp/output/README.txt new file mode 100644 index 0000000..3163ed4 --- /dev/null +++ b/testapp/output/README.txt @@ -0,0 +1,4 @@ +This directory contains files generated/saved by users as per their +username. The test executor will chdir into this user directory for each +user when they run the test. Do not delete this directory and ensure that +it is writeable by all. \ No newline at end of file diff --git a/testapp/settings.py b/testapp/settings.py new file mode 100644 index 0000000..f1c48b9 --- /dev/null +++ b/testapp/settings.py @@ -0,0 +1,188 @@ +# Django settings for tester project. + +from os.path import dirname, join, basename, abspath + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +# 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 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 +# service is running. It should be > 1024 and less < 65535 though. +SERVER_POOL_PORT = 53579 + +# Timeout for the code to run in seconds. This is an integer! +SERVER_TIMEOUT = 2 + +# The root of the URL, for example you might be in the situation where you +# are not hosted as host.org/exam/ but as host.org/foo/exam/ for whatever +# reason set this to the root you have to serve at. In the above example +# host.org/foo/exam set URL_ROOT='/foo' +URL_ROOT = '' + + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +CURDIR = abspath(dirname(__file__)) +DB_FILE = join(CURDIR, 'exam.db') + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': DB_FILE, # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'Asia/Kolkata' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '/tmp/static/' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = URL_ROOT + '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + join(CURDIR, 'static'), +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '9h*01@*#3ok+lbj5k=ym^eb)e=rf-g70&n0^nb_q6mtk!r(qr)' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = '%s.urls'%(basename(CURDIR)) + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + join(CURDIR, "templates"), +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Uncomment the next line to enable the admin: + 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', + 'south', + 'exam', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + }, + 'handlers': { + 'console':{ + 'level':'DEBUG', + 'class':'logging.StreamHandler', + 'formatter': 'simple' + }, + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins', 'console'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +AUTH_PROFILE_MODULE = 'exam.Profile' diff --git a/testapp/static/exam/css/base.css b/testapp/static/exam/css/base.css new file mode 100644 index 0000000..1323116 --- /dev/null +++ b/testapp/static/exam/css/base.css @@ -0,0 +1,26 @@ +body { font-family: 'Georgia', serif; font-size: 17px; color: #000; background: #eee;} +h1, h2, h3, h4 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; } +h1 { margin: 0 0 30px 0; font-size: 36px;} +h1 span { display: none; } +h2 { font-size: 26px; margin: 15px 0 5px 0; } +h3 { font-size: 22px; margin: 15px 0 5px 0; } +h4 { font-size: 15px; margin: 15px 0 5px 0; } + +.box { width: 700px; margin: 10px auto ; } +.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } +.entries { list-style: none; margin: 0; padding: 0; } +.entries li { margin: 0.8em 1.2em; } +.entries li h2 { margin-left: -1em; } +.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } +.add-entry dl { font-weight: bold; } +.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; + margin-bottom: 1em; background: #fafafa; } +.flash { background: #CEE5F5; padding: 0.5em; + border: 1px solid #AACBE2; } +.error { background: #F0D6D6; padding: 0.5em; } +textarea, code, +pre { font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', + monospace!important; font-size: 14px; background: #eee; } +pre { padding: 0px 30px; margin: 15px -30px; line-height: 1.3; } + diff --git a/testapp/templates/404.html b/testapp/templates/404.html new file mode 100644 index 0000000..7d33dd3 --- /dev/null +++ b/testapp/templates/404.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block content %} +The requested page does not exist. +{% endblock %} diff --git a/testapp/templates/500.html b/testapp/templates/500.html new file mode 100644 index 0000000..d02721f --- /dev/null +++ b/testapp/templates/500.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} +Internal Server error.
+This event will be reported.
+Sorry for the inconvinience. +{% endblock %} diff --git a/testapp/templates/base.html b/testapp/templates/base.html new file mode 100644 index 0000000..c2bbabb --- /dev/null +++ b/testapp/templates/base.html @@ -0,0 +1,25 @@ + + + + + +{% block title %} +{% endblock %} + +{% block meta %} +{% endblock %} + +{% block css %} +{% endblock %} +{% block script %} +{% endblock %} + + + +
+{% block content %} +{% endblock %} +
+ + diff --git a/testapp/templates/exam/complete.html b/testapp/templates/exam/complete.html new file mode 100644 index 0000000..4c3f3d5 --- /dev/null +++ b/testapp/templates/exam/complete.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block title %}Good bye!{% endblock %} + +{% block content %} +

Good bye!

+ +

{{message}}

+
+

You may now close the browser.

+ +{% endblock content %} diff --git a/testapp/templates/exam/grade_user.html b/testapp/templates/exam/grade_user.html new file mode 100644 index 0000000..75ed2e0 --- /dev/null +++ b/testapp/templates/exam/grade_user.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} + +{% block title %} Grading papers for {{ data.user.get_full_name.title }} {% endblock title %} + +{% block content %} + +

Grading papers for {{ data.user.get_full_name.title }}

+ +

+Name: {{ data.user.get_full_name.title }} +{% if data.profile %} +(roll number: {{ data.profile.roll_number }})
+{{ data.profile.position }}, +{{ data.profile.department }}, +{{ data.profile.institute }} +{% endif %} +

+ +{% if data.papers %} + +{% for paper in data.papers %} + +

Quiz: {{ paper.quiz.description }}

+ +

+Questions correctly answered: {{ paper.get_answered_str }}
+Total attempts at questions: {{ paper.answers.count }}
+Marks obtained: {{ paper.get_total_marks }}
+Start time: {{ paper.start_time }}
+

+ +{% if paper.answers.count %} +

Answers

+
+{% csrf_token %} +{% for question, answers in paper.get_question_answers.items %} +

+ + Question: {{ question.id }}. {{ question.summary }} + (Points: {{ question.points }})

+{% if question.type == "mcq" %} +

Choices: +{% for option in question.options.strip.splitlines %} {{option}}, {% endfor %} +

+

Student answer: {{ answers.0 }}

+{% else %}{# non-mcq questions #} +
+{% for answer in answers %}################################################################################
+{{ answer.answer.strip }}
+# Autocheck: {{ answer.error }}
+{% endfor %}
+{% endif %} {# if question.type #} +{% with answers|last as answer %} +Marks: +{% endwith %} +{% endfor %} {# for question, answers ... #} +

Teacher comments:

+ +
+ +
+{% endif %} {# if paper.answers.count #} + +{% endfor %} {# for paper in data.papers #} + +{% endif %} {# if data.papers #} + +{% if data.papers.count > 1 %} + + Monitor quiz +{% else %} +{% with data.papers.0 as paper %} + + Monitor quiz +{% endwith %} +{% endif %} +
+Admin +{% endblock content %} diff --git a/testapp/templates/exam/intro.html b/testapp/templates/exam/intro.html new file mode 100644 index 0000000..1d3e5de --- /dev/null +++ b/testapp/templates/exam/intro.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} + +{% block title %}Instructions and Rules {% endblock %} + +{% block content %} +

Important rules and instructions

+ +

Welcome {{user.first_name.title}} {{user.last_name.title}}, +to the programming quiz!

+ +

+This examination system has been developed with the intention of making you +learn programming and be assessed in an interactive and fun manner. +You will be presented with a series of programming questions and problems that +you will answer online and get immediate feedback for. +

+ +

Here are some important instructions and rules that you should understand +carefully. +

+ +
    + +
  • For any programming questions, you can submit solutions as many times as + you want without a penalty. You may skip questions and solve them later. +
  • + +
  • You may use your computer's Python/IPython shell or + an editor to solve + the problem and cut/paste the solution to the web interface. +
  • + +
  • You are not allowed to use any internet + resources, i.e. no google etc.
  • + +
  • Do not copy or share the questions or answers with anyone until the + exam is complete for everyone.
  • + +
  • All your attempts at the questions are logged. + Do not try to outsmart and break the testing system. If you do, we know + who you are and we will expell you from the course. You have been warned. +
  • + +
+ +

We hope you enjoy taking this exam.

+ +
+{% csrf_token %} + +
+ +{% endblock content %} \ No newline at end of file diff --git a/testapp/templates/exam/login.html b/testapp/templates/exam/login.html new file mode 100644 index 0000000..8e6352e --- /dev/null +++ b/testapp/templates/exam/login.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block title %}Login{% endblock title %} + +{% block content %} +

Welcome to the Examination. +Please login to proceed.

+ +
+{% csrf_token %} + + +{{ form.as_table }} +
+ + +
+ +New User Registration +{% endblock content %} \ No newline at end of file diff --git a/testapp/templates/exam/monitor.html b/testapp/templates/exam/monitor.html new file mode 100644 index 0000000..fb6cb58 --- /dev/null +++ b/testapp/templates/exam/monitor.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %} Quiz results {% endblock title %} + +{% block meta %} {% endblock meta %} + +{% block content %} + +{% if not quizzes and not quiz %} +

Quiz results

+ +

No quizzes available.

+ +{% endif %} + +{# ############################################################### #} +{# This is rendered when we are just viewing exam/monitor #} +{% if quizzes %} +

Available quizzes

+ + +{% endif %} + +{# ############################################################### #} +{# This is rendered when we are just viewing exam/monitor/quiz_num #} +{% if quiz %} +

{{ quiz.description }} results

+{% if papers %} +{#

Quiz: {{ quiz_name }}

#} +

Number of papers: {{ papers|length }}

+ + + + + + + + + + + + {% for paper in papers %} + + + + + + + + + + {% endfor %} +
Name Username Roll number Institute Questions answered Total marks Attempts
+ {{ paper.user.get_full_name.title }} + {{ paper.user.username }} {{ paper.profile.roll_number }} {{ paper.profile.institute }} {{ paper.get_answered_str }} {{ paper.get_total_marks }} {{ paper.answers.count }}
+{% else %} +

No answer papers so far.

+{% endif %} {# if papers #} +{% endif %} + +Admin + +{% endblock content %} diff --git a/testapp/templates/exam/question.html b/testapp/templates/exam/question.html new file mode 100644 index 0000000..8b589b6 --- /dev/null +++ b/testapp/templates/exam/question.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} + +{% block title %} Answer question {% endblock %} + +{% block script %} + +{% endblock script %} + +{% block onload %} onload="update_time()" {% endblock %} + +{% block content %} +

{{ question.summary }}

+ +

{{ question.description|safe }} +
+(Marks: {{ question.points }})

+ +{% if error_message %}

ERROR:

{{ error_message }}
{% endif %} + +

+ +
+{% csrf_token %} +{% if question.type == "mcq" %} +{% for option in question.options.strip.splitlines %} +{{option}}
+{% endfor %} +{% else %} + +{% endif %} +
+{% if question.type == "mcq" %} + +{% else %} + +{% endif %} + +
+ +

{{ user.first_name.title }} {{ user.last_name.title }}, +you have {{ paper.questions_left }} question(s) left in {{ quiz_name }}.

+ +

Time left:

+ +
+
+{% csrf_token %} + +
+ +{% endblock content %} diff --git a/testapp/templates/exam/quit.html b/testapp/templates/exam/quit.html new file mode 100644 index 0000000..37b5c08 --- /dev/null +++ b/testapp/templates/exam/quit.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block title %}Quit exam {% endblock %} + +{% block content %} +

Your current answers are saved.

+

Are you sure you wish to quit the exam?

+ +
+{% csrf_token %} + + +
+{% endblock content %} \ No newline at end of file diff --git a/testapp/templates/exam/register.html b/testapp/templates/exam/register.html new file mode 100644 index 0000000..921e7b5 --- /dev/null +++ b/testapp/templates/exam/register.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block title %}Registration form {% endblock %} + +{% block content %} +Please provide the following details. +
+{% csrf_token %} + + +{{ form.as_table }} +
+ + +
+ +{% endblock content %} \ No newline at end of file diff --git a/testapp/templates/exam/user_data.html b/testapp/templates/exam/user_data.html new file mode 100644 index 0000000..9fb442a --- /dev/null +++ b/testapp/templates/exam/user_data.html @@ -0,0 +1,84 @@ +{% extends "base.html" %} + +{% block title %} Data for user {{ data.user.get_full_name.title }} {% endblock title %} + +{% block content %} + +

Data for user {{ data.user.get_full_name.title }}

+ +

+Name: {{ data.user.get_full_name.title }}
+Username: {{ data.user.username }}
+{% if data.profile %} +Roll number: {{ data.profile.roll_number }}
+Position: {{ data.profile.position }}
+Department: {{ data.profile.department }}
+Institute: {{ data.profile.institute }}
+{% endif %} +Email: {{ data.user.email }}
+Date joined: {{ data.user.date_joined }}
+Last login: {{ data.user.last_login }} +

+ +{% if data.papers %} +

+ Grade/correct paper +

+ +{% for paper in data.papers %} + +

Quiz: {{ paper.quiz.description }}

+ +

+Questions correctly answered: {{ paper.get_answered_str }}
+Total attempts at questions: {{ paper.answers.count }}
+Marks obtained: {{ paper.get_total_marks }}
+Start time: {{ paper.start_time }}
+User IP address: {{ paper.user_ip }} +

+ +{% if paper.answers.count %} +

Answers

+{% for question, answers in paper.get_question_answers.items %} +

Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})

+{% if question.type == "mcq" %} +

Choices: +{% for option in question.options.strip.splitlines %} {{option}}, {% endfor %} +

+

Student answer: {{ answers.0 }}

+{% else %}{# non-mcq questions #} +
 
+{% for answer in answers %}################################################################################
+{{ answer.answer.strip }}
+# Autocheck: {{ answer.error }}
+{% endfor %}
+{% endif %} +{% with answers|last as answer %} +

Marks: {{answer.marks}}

+{% endwith %} +{% endfor %} {# for question, answers ... #} +

Teacher comments:

+{{ paper.comments|default:"None" }} +{% endif %} {# if paper.answers.count #} + +{% endfor %} {# for paper in data.papers #} + +{% endif %} {# if data.papers #} +
+
+ + Grade/correct paper +
+{% if data.papers.count > 1 %} + + Monitor quiz +{% else %} +{% with data.papers.0 as paper %} + + Monitor quiz +{% endwith %} +{% endif %} +
+Admin + +{% endblock content %} diff --git a/testapp/test_server.py b/testapp/test_server.py new file mode 100644 index 0000000..be9f876 --- /dev/null +++ b/testapp/test_server.py @@ -0,0 +1,83 @@ +"""Simple test suite for the code server. Running this requires that one start +up the code server as:: + + $ sudo ./code_server.py + +""" +from exam.xmlrpc_clients import code_server + +def check_result(result, check='correct answer'): + if check != 'correct answer': + assert result[0] == False + else: + assert result[0] == True + if "unable to connect" in result[1].lower(): + assert result[0], result[1] + assert check in result[1].lower(), result[1] + +def test_python(): + """Test if server runs Python code as expected.""" + src = 'while True: pass' + result = code_server.run_code(src, '', '/tmp', language="python") + check_result(result, 'more than ') + + src = 'x = 1' + result = code_server.run_code(src, 'assert x == 1', '/tmp', + language="python") + check_result(result, 'correct answer') + + result = code_server.run_code(src, 'assert x == 0', '/tmp', + language="python") + check_result(result, 'assertionerror') + + src = 'abracadabra' + result = code_server.run_code(src, 'assert x == 0', '/tmp', + language="python") + check_result(result, 'nameerror') + +def test_bash(): + """Test if server runs Bash code as expected.""" + src = """ +#!/bin/bash + [[ $# -eq 2 ]] && echo $(( $1 + $2 )) && exit $(( $1 + $2 )) + """ + result = code_server.run_code(src, + 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") + check_result(result) + + src = """ +#!/bin/bash + [[ $# -eq 2 ]] && echo $(( $1 - $2 )) && exit $(( $1 - $2 )) + """ + result = code_server.run_code(src, + 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") + check_result(result, 'error') + + src = """\ +#!/bin/bash + while [ 1 ] ; do echo "" > /dev/null ; done + """ + result = code_server.run_code(src, + 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") + check_result(result, 'more than ') + + src = ''' +#!/bin/bash + while [ 1 ] ; do echo "" > /dev/null + ''' + result = code_server.run_code(src, + 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") + check_result(result, 'error') + + src = '''# Enter your code here. +#!/bin/bash + while [ 1 ] ; do echo "" > /dev/null + ''' + result = code_server.run_code(src, + 'docs/sample.sh\ndocs/sample.args', '/tmp', language="bash") + check_result(result, 'oserror') + +if __name__ == '__main__': + test_python() + test_bash() + diff --git a/testapp/urls.py b/testapp/urls.py new file mode 100644 index 0000000..d956bfc --- /dev/null +++ b/testapp/urls.py @@ -0,0 +1,23 @@ +from django.conf.urls.defaults import patterns, include, url + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +from settings import URL_ROOT + +if URL_ROOT.startswith('/'): + URL_BASE = r'^%s/exam/'%URL_ROOT[1:] + ADMIN_BASE = r'^%s/admin/'%URL_ROOT[1:] +else: + URL_BASE = r'^exam/' + ADMIN_BASE = r'^admin/' + +urlpatterns = patterns('', + url(URL_BASE, include('exam.urls')), + + # Uncomment the admin/doc line below to enable admin documentation: + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + # Uncomment the next line to enable the admin: + url(ADMIN_BASE, include(admin.site.urls)), +) diff --git a/urls.py b/urls.py deleted file mode 100644 index d956bfc..0000000 --- a/urls.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.conf.urls.defaults import patterns, include, url - -# Uncomment the next two lines to enable the admin: -from django.contrib import admin -admin.autodiscover() - -from settings import URL_ROOT - -if URL_ROOT.startswith('/'): - URL_BASE = r'^%s/exam/'%URL_ROOT[1:] - ADMIN_BASE = r'^%s/admin/'%URL_ROOT[1:] -else: - URL_BASE = r'^exam/' - ADMIN_BASE = r'^admin/' - -urlpatterns = patterns('', - url(URL_BASE, include('exam.urls')), - - # Uncomment the admin/doc line below to enable admin documentation: - # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - # Uncomment the next line to enable the admin: - url(ADMIN_BASE, include(admin.site.urls)), -) -- cgit From 5d600ad389d50832fac95e7ac381d4eba7ee70ba Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 04:05:26 +0530 Subject: Added bootstrap and buildout --- bootstrap.py | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ buildout.cfg | 21 +++++ 2 files changed, 283 insertions(+) create mode 100644 bootstrap.py create mode 100644 buildout.cfg diff --git a/bootstrap.py b/bootstrap.py new file mode 100644 index 0000000..7647cbb --- /dev/null +++ b/bootstrap.py @@ -0,0 +1,262 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os, shutil, sys, tempfile, urllib, urllib2, subprocess +from optparse import OptionParser + +if sys.platform == 'win32': + def quote(c): + if ' ' in c: + return '"%s"' % c # work around spawn lamosity on windows + else: + return c +else: + quote = str + +# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. +stdout, stderr = subprocess.Popen( + [sys.executable, '-Sc', + 'try:\n' + ' import ConfigParser\n' + 'except ImportError:\n' + ' print 1\n' + 'else:\n' + ' print 0\n'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() +has_broken_dash_S = bool(int(stdout.strip())) + +# In order to be more robust in the face of system Pythons, we want to +# run without site-packages loaded. This is somewhat tricky, in +# particular because Python 2.6's distutils imports site, so starting +# with the -S flag is not sufficient. However, we'll start with that: +if not has_broken_dash_S and 'site' in sys.modules: + # We will restart with python -S. + args = sys.argv[:] + args[0:0] = [sys.executable, '-S'] + args = map(quote, args) + os.execv(sys.executable, args) +# Now we are running with -S. We'll get the clean sys.path, import site +# because distutils will do it later, and then reset the path and clean +# out any namespace packages from site-packages that might have been +# loaded by .pth files. +clean_path = sys.path[:] +import site # imported because of its side effects +sys.path[:] = clean_path +for k, v in sys.modules.items(): + if k in ('setuptools', 'pkg_resources') or ( + hasattr(v, '__path__') and + len(v.__path__) == 1 and + not os.path.exists(os.path.join(v.__path__[0], '__init__.py'))): + # This is a namespace package. Remove it. + sys.modules.pop(k) + +is_jython = sys.platform.startswith('java') + +setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' +distribute_source = 'http://python-distribute.org/distribute_setup.py' + + +# parsing arguments +def normalize_to_url(option, opt_str, value, parser): + if value: + if '://' not in value: # It doesn't smell like a URL. + value = 'file://%s' % ( + urllib.pathname2url( + os.path.abspath(os.path.expanduser(value))),) + if opt_str == '--download-base' and not value.endswith('/'): + # Download base needs a trailing slash to make the world happy. + value += '/' + else: + value = None + name = opt_str[2:].replace('-', '_') + setattr(parser.values, name, value) + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +Simply run this script in a directory containing a buildout.cfg, using the +Python that you want bin/buildout to use. + +Note that by using --setup-source and --download-base to point to +local resources, you can keep this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", dest="version", + help="use a specific zc.buildout version") +parser.add_option("-d", "--distribute", + action="store_true", dest="use_distribute", default=False, + help="Use Distribute rather than Setuptools.") +parser.add_option("--setup-source", action="callback", dest="setup_source", + callback=normalize_to_url, nargs=1, type="string", + help=("Specify a URL or file location for the setup file. " + "If you use Setuptools, this will default to " + + setuptools_source + "; if you use Distribute, this " + "will default to " + distribute_source + ".")) +parser.add_option("--download-base", action="callback", dest="download_base", + callback=normalize_to_url, nargs=1, type="string", + help=("Specify a URL or directory for downloading " + "zc.buildout and either Setuptools or Distribute. " + "Defaults to PyPI.")) +parser.add_option("--eggs", + help=("Specify a directory for storing eggs. Defaults to " + "a temporary directory that is deleted when the " + "bootstrap script completes.")) +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", None, action="store", dest="config_file", + help=("Specify the path to the buildout configuration " + "file to be used.")) + +options, args = parser.parse_args() + +# if -c was provided, we push it back into args for buildout's main function +if options.config_file is not None: + args += ['-c', options.config_file] + +if options.eggs: + eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) +else: + eggs_dir = tempfile.mkdtemp() + +if options.setup_source is None: + if options.use_distribute: + options.setup_source = distribute_source + else: + options.setup_source = setuptools_source + +if options.accept_buildout_test_releases: + args.append('buildout:accept-buildout-test-releases=true') +args.append('bootstrap') + +try: + import pkg_resources + import setuptools # A flag. Sometimes pkg_resources is installed alone. + if not hasattr(pkg_resources, '_distribute'): + raise ImportError +except ImportError: + ez_code = urllib2.urlopen( + options.setup_source).read().replace('\r\n', '\n') + ez = {} + exec ez_code in ez + setup_args = dict(to_dir=eggs_dir, download_delay=0) + if options.download_base: + setup_args['download_base'] = options.download_base + if options.use_distribute: + setup_args['no_fake'] = True + ez['use_setuptools'](**setup_args) + if 'pkg_resources' in sys.modules: + reload(sys.modules['pkg_resources']) + import pkg_resources + # This does not (always?) update the default working set. We will + # do it. + for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +cmd = [quote(sys.executable), + '-c', + quote('from setuptools.command.easy_install import main; main()'), + '-mqNxd', + quote(eggs_dir)] + +if not has_broken_dash_S: + cmd.insert(1, '-S') + +find_links = options.download_base +if not find_links: + find_links = os.environ.get('bootstrap-testing-find-links') +if find_links: + cmd.extend(['-f', quote(find_links)]) + +if options.use_distribute: + setup_requirement = 'distribute' +else: + setup_requirement = 'setuptools' +ws = pkg_resources.working_set +setup_requirement_path = ws.find( + pkg_resources.Requirement.parse(setup_requirement)).location +env = dict( + os.environ, + PYTHONPATH=setup_requirement_path) + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _final_parts = '*final-', '*final' + + def _final_version(parsed_version): + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + index = setuptools.package_index.PackageIndex( + search_path=[setup_requirement_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version +if version: + requirement = '=='.join((requirement, version)) +cmd.append(requirement) + +if is_jython: + import subprocess + exitcode = subprocess.Popen(cmd, env=env).wait() +else: # Windows prefers this, apparently; otherwise we would prefer subprocess + exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) +if exitcode != 0: + sys.stdout.flush() + sys.stderr.flush() + print ("An error occurred when trying to install zc.buildout. " + "Look above this message for any errors that " + "were output by easy_install.") + sys.exit(exitcode) + +ws.add_entry(eggs_dir) +ws.require(requirement) +import zc.buildout.buildout +zc.buildout.buildout.main(args) +if not options.eggs: # clean up temporary egg directory + shutil.rmtree(eggs_dir) diff --git a/buildout.cfg b/buildout.cfg new file mode 100644 index 0000000..8d62fa5 --- /dev/null +++ b/buildout.cfg @@ -0,0 +1,21 @@ +[buildout] +parts = + django +eggs = + PIL + South + Werkzeug + +[versions] +django = 1.3 + +[django] +recipe = djangorecipe +project = testapp +settings = settings +wsgi = true +wsgilog=wsgi.log +eggs = + ${buildout:eggs} +extra-paths = + testapp -- cgit From 78546195b033adf76e6dea50798396f987f5e019 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 04:06:30 +0530 Subject: Modified the .gitignore to not track buildout files --- .gitignore | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.gitignore b/.gitignore index 3eeee5b..3e88586 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,43 @@ *.pyc +*.zip +*~ +.project +.pydevproject +app.yaml +build +tests/coverageResults +*,cover +tests/.coverage +*.git +*.egg-info +eggs +parts +.installed.cfg +bin +develop-eggs +.DS_Store +.settings +.*.swp +*.egg +*.egg-link +*.svn* +*.wpr +.installed.cfg +src/* +parts +downloads +.xvpics* +.project +pytask/local.py +pytask/pytask.egg-info +pytask/static/media +pytask/uploads +apache/* +*.swp +*.bak +*.swo +*.db-journal *.db +migrations +wsgi.log -- cgit From ca3551e435ad9ac1f3a06161c3289227ccea0b09 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 04:18:46 +0530 Subject: modified the bootstrap to get the setup tools --- bootstrap.py | 267 +++++++++-------------------------------------------------- 1 file changed, 41 insertions(+), 226 deletions(-) diff --git a/bootstrap.py b/bootstrap.py index 7647cbb..7728587 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -1,6 +1,6 @@ ############################################################################## # -# Copyright (c) 2006 Zope Foundation and Contributors. +# Copyright (c) 2006 Zope Corporation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, @@ -16,247 +16,62 @@ Simply run this script in a directory containing a buildout.cfg. The script accepts buildout command-line options, so you can use the -c option to specify an alternate configuration file. -""" - -import os, shutil, sys, tempfile, urllib, urllib2, subprocess -from optparse import OptionParser -if sys.platform == 'win32': - def quote(c): - if ' ' in c: - return '"%s"' % c # work around spawn lamosity on windows - else: - return c -else: - quote = str +$Id$ +""" -# See zc.buildout.easy_install._has_broken_dash_S for motivation and comments. -stdout, stderr = subprocess.Popen( - [sys.executable, '-Sc', - 'try:\n' - ' import ConfigParser\n' - 'except ImportError:\n' - ' print 1\n' - 'else:\n' - ' print 0\n'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() -has_broken_dash_S = bool(int(stdout.strip())) +import os, shutil, sys, tempfile, urllib2 -# In order to be more robust in the face of system Pythons, we want to -# run without site-packages loaded. This is somewhat tricky, in -# particular because Python 2.6's distutils imports site, so starting -# with the -S flag is not sufficient. However, we'll start with that: -if not has_broken_dash_S and 'site' in sys.modules: - # We will restart with python -S. - args = sys.argv[:] - args[0:0] = [sys.executable, '-S'] - args = map(quote, args) - os.execv(sys.executable, args) -# Now we are running with -S. We'll get the clean sys.path, import site -# because distutils will do it later, and then reset the path and clean -# out any namespace packages from site-packages that might have been -# loaded by .pth files. -clean_path = sys.path[:] -import site # imported because of its side effects -sys.path[:] = clean_path -for k, v in sys.modules.items(): - if k in ('setuptools', 'pkg_resources') or ( - hasattr(v, '__path__') and - len(v.__path__) == 1 and - not os.path.exists(os.path.join(v.__path__[0], '__init__.py'))): - # This is a namespace package. Remove it. - sys.modules.pop(k) +tmpeggs = tempfile.mkdtemp() is_jython = sys.platform.startswith('java') -setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py' -distribute_source = 'http://python-distribute.org/distribute_setup.py' - - -# parsing arguments -def normalize_to_url(option, opt_str, value, parser): - if value: - if '://' not in value: # It doesn't smell like a URL. - value = 'file://%s' % ( - urllib.pathname2url( - os.path.abspath(os.path.expanduser(value))),) - if opt_str == '--download-base' and not value.endswith('/'): - # Download base needs a trailing slash to make the world happy. - value += '/' - else: - value = None - name = opt_str[2:].replace('-', '_') - setattr(parser.values, name, value) - -usage = '''\ -[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] - -Bootstraps a buildout-based project. - -Simply run this script in a directory containing a buildout.cfg, using the -Python that you want bin/buildout to use. - -Note that by using --setup-source and --download-base to point to -local resources, you can keep this script from going over the network. -''' - -parser = OptionParser(usage=usage) -parser.add_option("-v", "--version", dest="version", - help="use a specific zc.buildout version") -parser.add_option("-d", "--distribute", - action="store_true", dest="use_distribute", default=False, - help="Use Distribute rather than Setuptools.") -parser.add_option("--setup-source", action="callback", dest="setup_source", - callback=normalize_to_url, nargs=1, type="string", - help=("Specify a URL or file location for the setup file. " - "If you use Setuptools, this will default to " + - setuptools_source + "; if you use Distribute, this " - "will default to " + distribute_source + ".")) -parser.add_option("--download-base", action="callback", dest="download_base", - callback=normalize_to_url, nargs=1, type="string", - help=("Specify a URL or directory for downloading " - "zc.buildout and either Setuptools or Distribute. " - "Defaults to PyPI.")) -parser.add_option("--eggs", - help=("Specify a directory for storing eggs. Defaults to " - "a temporary directory that is deleted when the " - "bootstrap script completes.")) -parser.add_option("-t", "--accept-buildout-test-releases", - dest='accept_buildout_test_releases', - action="store_true", default=False, - help=("Normally, if you do not specify a --version, the " - "bootstrap script and buildout gets the newest " - "*final* versions of zc.buildout and its recipes and " - "extensions for you. If you use this flag, " - "bootstrap and buildout will get the newest releases " - "even if they are alphas or betas.")) -parser.add_option("-c", None, action="store", dest="config_file", - help=("Specify the path to the buildout configuration " - "file to be used.")) - -options, args = parser.parse_args() - -# if -c was provided, we push it back into args for buildout's main function -if options.config_file is not None: - args += ['-c', options.config_file] - -if options.eggs: - eggs_dir = os.path.abspath(os.path.expanduser(options.eggs)) -else: - eggs_dir = tempfile.mkdtemp() - -if options.setup_source is None: - if options.use_distribute: - options.setup_source = distribute_source - else: - options.setup_source = setuptools_source - -if options.accept_buildout_test_releases: - args.append('buildout:accept-buildout-test-releases=true') -args.append('bootstrap') - try: import pkg_resources - import setuptools # A flag. Sometimes pkg_resources is installed alone. - if not hasattr(pkg_resources, '_distribute'): - raise ImportError except ImportError: - ez_code = urllib2.urlopen( - options.setup_source).read().replace('\r\n', '\n') ez = {} - exec ez_code in ez - setup_args = dict(to_dir=eggs_dir, download_delay=0) - if options.download_base: - setup_args['download_base'] = options.download_base - if options.use_distribute: - setup_args['no_fake'] = True - ez['use_setuptools'](**setup_args) - if 'pkg_resources' in sys.modules: - reload(sys.modules['pkg_resources']) - import pkg_resources - # This does not (always?) update the default working set. We will - # do it. - for path in sys.path: - if path not in pkg_resources.working_set.entries: - pkg_resources.working_set.add_entry(path) - -cmd = [quote(sys.executable), - '-c', - quote('from setuptools.command.easy_install import main; main()'), - '-mqNxd', - quote(eggs_dir)] - -if not has_broken_dash_S: - cmd.insert(1, '-S') + exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' + ).read() in ez + ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) -find_links = options.download_base -if not find_links: - find_links = os.environ.get('bootstrap-testing-find-links') -if find_links: - cmd.extend(['-f', quote(find_links)]) + import pkg_resources -if options.use_distribute: - setup_requirement = 'distribute' +if sys.platform == 'win32': + def quote(c): + if ' ' in c: + return '"%s"' % c # work around spawn lamosity on windows + else: + return c else: - setup_requirement = 'setuptools' -ws = pkg_resources.working_set -setup_requirement_path = ws.find( - pkg_resources.Requirement.parse(setup_requirement)).location -env = dict( - os.environ, - PYTHONPATH=setup_requirement_path) + def quote (c): + return c -requirement = 'zc.buildout' -version = options.version -if version is None and not options.accept_buildout_test_releases: - # Figure out the most recent final version of zc.buildout. - import setuptools.package_index - _final_parts = '*final-', '*final' - - def _final_version(parsed_version): - for part in parsed_version: - if (part[:1] == '*') and (part not in _final_parts): - return False - return True - index = setuptools.package_index.PackageIndex( - search_path=[setup_requirement_path]) - if find_links: - index.add_find_links((find_links,)) - req = pkg_resources.Requirement.parse(requirement) - if index.obtain(req) is not None: - best = [] - bestv = None - for dist in index[req.project_name]: - distv = dist.parsed_version - if _final_version(distv): - if bestv is None or distv > bestv: - best = [dist] - bestv = distv - elif distv == bestv: - best.append(dist) - if best: - best.sort() - version = best[-1].version -if version: - requirement = '=='.join((requirement, version)) -cmd.append(requirement) +cmd = 'from setuptools.command.easy_install import main; main()' +ws = pkg_resources.working_set if is_jython: import subprocess - exitcode = subprocess.Popen(cmd, env=env).wait() -else: # Windows prefers this, apparently; otherwise we would prefer subprocess - exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env])) -if exitcode != 0: - sys.stdout.flush() - sys.stderr.flush() - print ("An error occurred when trying to install zc.buildout. " - "Look above this message for any errors that " - "were output by easy_install.") - sys.exit(exitcode) + + assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', + quote(tmpeggs), 'zc.buildout'], + env=dict(os.environ, + PYTHONPATH= + ws.find(pkg_resources.Requirement.parse('setuptools')).location + ), + ).wait() == 0 -ws.add_entry(eggs_dir) -ws.require(requirement) +else: + assert os.spawnle( + os.P_WAIT, sys.executable, quote (sys.executable), + '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout', + dict(os.environ, + PYTHONPATH= + ws.find(pkg_resources.Requirement.parse('setuptools')).location + ), + ) == 0 + +ws.add_entry(tmpeggs) +ws.require('zc.buildout') import zc.buildout.buildout -zc.buildout.buildout.main(args) -if not options.eggs: # clean up temporary egg directory - shutil.rmtree(eggs_dir) +zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap']) +shutil.rmtree(tmpeggs) -- cgit From 864e4f98586f9cadea9f1695c641d1c963596b01 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 04:21:49 +0530 Subject: Removed the apache directory as the wsgi script is automatically made by buildout in /bin/django.wsgi --- testapp/apache/django.wsgi | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 testapp/apache/django.wsgi diff --git a/testapp/apache/django.wsgi b/testapp/apache/django.wsgi deleted file mode 100644 index ef88526..0000000 --- a/testapp/apache/django.wsgi +++ /dev/null @@ -1,18 +0,0 @@ -import os -from os.path import dirname, abspath -import sys - -# This file is inside online_test/apache/django.wsgi -# pth should be online_test -pth = abspath(dirname(dirname(__file__))) -if pth not in sys.path: - sys.path.append(pth) -# Now add the parent of online_test also. -pth = dirname(pth) -if pth not in sys.path: - sys.path.append(pth) - -os.environ['DJANGO_SETTINGS_MODULE'] = 'online_test.settings' - -import django.core.handlers.wsgi -application = django.core.handlers.wsgi.WSGIHandler() -- cgit From 79dd8c309e2f847360442290aa1f5321ea790b16 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 04:23:22 +0530 Subject: Moved the README file to the root dir --- README.txt | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++++ testapp/README.txt | 190 ----------------------------------------------------- 2 files changed, 190 insertions(+), 190 deletions(-) create mode 100644 README.txt delete mode 100644 testapp/README.txt diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..a265675 --- /dev/null +++ b/README.txt @@ -0,0 +1,190 @@ +Introduction +============ + +This app provides an "exam" app that lets users take an online +programming quiz. Currently only Python and simple Bash scripts can be +tested. At FOSSEE, Nishanth had implemented a nice django based app to +test for multiple-choice questions. However, I was inspired by a +programming contest that I saw at PyCon APAC 2011. Chris Boesch, who +administered the contest, used a nice web application that he had built +on top of GAE that basically checked your Python code, live. This made +it fun and interesting. Their application can be seen at +http://singpath.com + +I wanted an implementation that was not tied to GAE and decided to write +one myself and the result is the "exam" app. The idea being that I can +use this to test students programming skills and not have to worry about +grading their answers myself and I can do so on my machines. + +You can define fairly complicated programming problems and have users +solve the problem and the solution is checked immediately. The system +supports pretty much arbitrary Python and uses "test cases" to test the +implementations of the students. It also supports simple bash scripts +-- see the sample questions in "docs/". In addition it supports simple +multiple choice questions. Since it runs on your Python, you could +technically test any Python based library. It is distributed under the +BSD license. + +It can use a lot more work but the basics work and the app scales to +over 500+ simultaneous users. :) + +Dependencies +============= + +Before you install/deploy, make sure you have the following installed: + + - Django 1.3 or above. + - South (tested with 0.7.3). + +That and a running Python is pretty much all you need. Of course, for +serious deployment you are going to need Apache or some other decent +webserver. + + +Installation and Deployment +============================= + +To install/deploy this app follow the steps below: + + 1. Clone this repository and cd to the cloned repo. + + 2. Run:: + + $ python manage.py syncdb + [ enter password etc.] + + $ python manage.py migrate exam + + 3. Add questions by editing the "docs/sample_questions.py" or any other + file in the same format and then run the following:: + + $ python manage.py load_exam docs/sample_questions.py + + Note that you can supply multiple Python files as arguments and all of + those will be added to the database. + + 4. First run the python server provided. This ensures that the code is + executed in a safe environment. Do this like so:: + + $ sudo python code_server.py + + Put this in the background once it has started since this will not + return back the prompt. It is important that the server be running + *before* students start attempting the exam. Using sudo is + necessary since the server is run as the user "nobody". This runs + on the ports configured in the settings.py file in the variable + "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. + This is the maximum time allowed to execute the submitted code. + Note that this will likely spawn multiple processes as "nobody" + depending on the number of server ports specified. + + 5. Now, run:: + + $ python manage.py runserver : + + For deployment use Apache or a real webserver, see below for more + information. + + 6. Go to http://deserved_host_or_ip:desired_port/admin + + 7. Login with your credentials and look at the questions and modify if + needed. Create a new Quiz, set the date and duration or + activate/deactivate the quiz. + + 8. Now ask users to login at: + + http://host:port/exam + + And you should be all set. + + 9. Note that the directory "output" will contain directories, one for each + user. Users can potentially write output into these that can be used + for checking later. + + 10. As admin user you can visit http://host/exam/monitor to view + results and user data interactively. You could also "grade" the + papers manually if needed. + + 11. You may dump the results and user data using the results2csv and + dump_user_data commands. + +WARNING: django is running in debug mode for this currently, CHANGE it +during deployment. To do this, edit settings.py and set DEBUG to False. +Also look at other settings and change them suitably. + +The file docs/sample_questions.py is a template that you can use for your +own questions. + +Additional commands available +============================== + +We provide several convenient commands for you to use: + + - load_exam : load questions and a quiz from a python file. See + docs/sample_questions.py + + - load_questions_xml : load questions from XML file, see + docs/sample_questions.xml use of this is deprecated in favor of + load_exam. + + - results2csv : Dump the quiz results into a CSV file for further + processing. + + - dump_user_data : Dump out relevalt user data for either all users or + specified users. + +For more information on these do this:: + + $ ./manage.py help [command] + +where [command] is one of the above. + +Deploying via Apache +===================== + +For any serious deployment, you will need to deploy the app using a real +webserver like Apache. The ``apache/django.wsgi`` script should make it +easy to deploy this using mod_wsgi. You will need to add a line of the +form: + + WSGIScriptAlias / "/var/www/online_test/apache/django.wsgi" + +to your apache.conf. For more details see the Django docs here: + +https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ + + +Sometimes you might be in the situation where you are not hosted as +"host.org/exam/" but as "host.org/foo/exam/" for whatever reason. In +this case edit "settings.py" and set the "URL_ROOT" to the root you +have to serve at. In the above example for "host.org/foo/exam" set +URL_ROOT='/foo'. + +License +======= + +This is distributed under the terms of the BSD license. Copyright +information is at the bottom of this file. + +Authors +======= + +Main author: Prabhu Ramachandran + +I gratefully acknowledge help from the following: + + - Nishanth Amuluru originally from FOSSEE who wrote bulk of the + login/registration code. He wrote an initial first cut of a quiz app + which supported only simple questions which provided motivation for + this app. The current codebase does not share too much from his + implementation although there are plenty of similarities. + + - Harish Badrinath (FOSSEE) -- who provided a first cut of the bash + related scripts. + + - Srikant Patnaik and Thomas Stephen Lee, who helped deploy and test + the code. + + +Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) + diff --git a/testapp/README.txt b/testapp/README.txt deleted file mode 100644 index a265675..0000000 --- a/testapp/README.txt +++ /dev/null @@ -1,190 +0,0 @@ -Introduction -============ - -This app provides an "exam" app that lets users take an online -programming quiz. Currently only Python and simple Bash scripts can be -tested. At FOSSEE, Nishanth had implemented a nice django based app to -test for multiple-choice questions. However, I was inspired by a -programming contest that I saw at PyCon APAC 2011. Chris Boesch, who -administered the contest, used a nice web application that he had built -on top of GAE that basically checked your Python code, live. This made -it fun and interesting. Their application can be seen at -http://singpath.com - -I wanted an implementation that was not tied to GAE and decided to write -one myself and the result is the "exam" app. The idea being that I can -use this to test students programming skills and not have to worry about -grading their answers myself and I can do so on my machines. - -You can define fairly complicated programming problems and have users -solve the problem and the solution is checked immediately. The system -supports pretty much arbitrary Python and uses "test cases" to test the -implementations of the students. It also supports simple bash scripts --- see the sample questions in "docs/". In addition it supports simple -multiple choice questions. Since it runs on your Python, you could -technically test any Python based library. It is distributed under the -BSD license. - -It can use a lot more work but the basics work and the app scales to -over 500+ simultaneous users. :) - -Dependencies -============= - -Before you install/deploy, make sure you have the following installed: - - - Django 1.3 or above. - - South (tested with 0.7.3). - -That and a running Python is pretty much all you need. Of course, for -serious deployment you are going to need Apache or some other decent -webserver. - - -Installation and Deployment -============================= - -To install/deploy this app follow the steps below: - - 1. Clone this repository and cd to the cloned repo. - - 2. Run:: - - $ python manage.py syncdb - [ enter password etc.] - - $ python manage.py migrate exam - - 3. Add questions by editing the "docs/sample_questions.py" or any other - file in the same format and then run the following:: - - $ python manage.py load_exam docs/sample_questions.py - - Note that you can supply multiple Python files as arguments and all of - those will be added to the database. - - 4. First run the python server provided. This ensures that the code is - executed in a safe environment. Do this like so:: - - $ sudo python code_server.py - - Put this in the background once it has started since this will not - return back the prompt. It is important that the server be running - *before* students start attempting the exam. Using sudo is - necessary since the server is run as the user "nobody". This runs - on the ports configured in the settings.py file in the variable - "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. - This is the maximum time allowed to execute the submitted code. - Note that this will likely spawn multiple processes as "nobody" - depending on the number of server ports specified. - - 5. Now, run:: - - $ python manage.py runserver : - - For deployment use Apache or a real webserver, see below for more - information. - - 6. Go to http://deserved_host_or_ip:desired_port/admin - - 7. Login with your credentials and look at the questions and modify if - needed. Create a new Quiz, set the date and duration or - activate/deactivate the quiz. - - 8. Now ask users to login at: - - http://host:port/exam - - And you should be all set. - - 9. Note that the directory "output" will contain directories, one for each - user. Users can potentially write output into these that can be used - for checking later. - - 10. As admin user you can visit http://host/exam/monitor to view - results and user data interactively. You could also "grade" the - papers manually if needed. - - 11. You may dump the results and user data using the results2csv and - dump_user_data commands. - -WARNING: django is running in debug mode for this currently, CHANGE it -during deployment. To do this, edit settings.py and set DEBUG to False. -Also look at other settings and change them suitably. - -The file docs/sample_questions.py is a template that you can use for your -own questions. - -Additional commands available -============================== - -We provide several convenient commands for you to use: - - - load_exam : load questions and a quiz from a python file. See - docs/sample_questions.py - - - load_questions_xml : load questions from XML file, see - docs/sample_questions.xml use of this is deprecated in favor of - load_exam. - - - results2csv : Dump the quiz results into a CSV file for further - processing. - - - dump_user_data : Dump out relevalt user data for either all users or - specified users. - -For more information on these do this:: - - $ ./manage.py help [command] - -where [command] is one of the above. - -Deploying via Apache -===================== - -For any serious deployment, you will need to deploy the app using a real -webserver like Apache. The ``apache/django.wsgi`` script should make it -easy to deploy this using mod_wsgi. You will need to add a line of the -form: - - WSGIScriptAlias / "/var/www/online_test/apache/django.wsgi" - -to your apache.conf. For more details see the Django docs here: - -https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ - - -Sometimes you might be in the situation where you are not hosted as -"host.org/exam/" but as "host.org/foo/exam/" for whatever reason. In -this case edit "settings.py" and set the "URL_ROOT" to the root you -have to serve at. In the above example for "host.org/foo/exam" set -URL_ROOT='/foo'. - -License -======= - -This is distributed under the terms of the BSD license. Copyright -information is at the bottom of this file. - -Authors -======= - -Main author: Prabhu Ramachandran - -I gratefully acknowledge help from the following: - - - Nishanth Amuluru originally from FOSSEE who wrote bulk of the - login/registration code. He wrote an initial first cut of a quiz app - which supported only simple questions which provided motivation for - this app. The current codebase does not share too much from his - implementation although there are plenty of similarities. - - - Harish Badrinath (FOSSEE) -- who provided a first cut of the bash - related scripts. - - - Srikant Patnaik and Thomas Stephen Lee, who helped deploy and test - the code. - - -Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) - -- cgit From 6cde8b5c4bfcfa64f537dde395dacc44fda72bc7 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:01:34 +0530 Subject: Removed Werkzeug --- buildout.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/buildout.cfg b/buildout.cfg index 8d62fa5..c4a76b4 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -4,7 +4,6 @@ parts = eggs = PIL South - Werkzeug [versions] django = 1.3 -- cgit From 43b84eb21c4aee9fcfd24aee09c8072a1b4885da Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:03:33 +0530 Subject: Added separate production.cfg and production.py files for easy development --- production.cfg | 21 +++++++++++++++++++++ testapp/production.py | 10 ++++++++++ 2 files changed, 31 insertions(+) create mode 100644 production.cfg create mode 100644 testapp/production.py diff --git a/production.cfg b/production.cfg new file mode 100644 index 0000000..e3ee886 --- /dev/null +++ b/production.cfg @@ -0,0 +1,21 @@ +[buildout] +parts = + django +eggs = + PIL + South + MySQL-python + +[versions] +django = 1.3 + +[django] +recipe = djangorecipe +project = testapp +settings = production +wsgi = true +wsgilog=wsgi.log +eggs = + ${buildout:eggs} +extra-paths = + testapp diff --git a/testapp/production.py b/testapp/production.py new file mode 100644 index 0000000..2710bdb --- /dev/null +++ b/testapp/production.py @@ -0,0 +1,10 @@ +from project.settings import * + +DEBUG=False +TEMPLATE_DEBUG=DEBUG + +DATABASE_ENGINE = 'django.db.backends.mysql' +DATABASE_NAME = 'online_test' +DATABASE_USER = 'online_test_user' +# Imports DATABASE_PASSWORD from testapp/local.py that is not part of git repo +from testapp.local import DATABASE_PASSWORD \ No newline at end of file -- cgit From a10b683a851485466cac3dcbbb9ac08a95184a1c Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:19:28 +0530 Subject: Modified the README.txt file to give information on buildout --- README.txt | 153 ++++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 102 insertions(+), 51 deletions(-) diff --git a/README.txt b/README.txt index a265675..75d3061 100644 --- a/README.txt +++ b/README.txt @@ -28,37 +28,116 @@ BSD license. It can use a lot more work but the basics work and the app scales to over 500+ simultaneous users. :) -Dependencies +Pre-Requisite ============= -Before you install/deploy, make sure you have the following installed: - - - Django 1.3 or above. - - South (tested with 0.7.3). - -That and a running Python is pretty much all you need. Of course, for -serious deployment you are going to need Apache or some other decent -webserver. + 1. Install MySql Server + + 2. Install Python MySql support + + 3. Install Apache Server for deployment + +Configure MySql server +---------------------- + 1. Create a database named `online_test` + + 2. Add a user named `online_test_user` and give access to it on the database `online_test` + + 3. Create a file named `local.py` in folder `testapp` and insert `DATABASE_PASSWORD = 'yourpassword'` + -Installation and Deployment -============================= +Production Deployment +===================== To install/deploy this app follow the steps below: 1. Clone this repository and cd to the cloned repo. + + 2. run python bootstrap.py + + 3. run ./bin/buildout -c production.cfg + + 4. run ./bin/django syncdb + [ enter password etc.] + + run ./bin/django migrate exam + + 5. Add questions by editing the "docs/sample_questions.py" or any other + file in the same format and then run the following:: + + ./bin/django load_exam docs/sample_questions.py + + Note that you can supply multiple Python files as arguments and all of + those will be added to the database. + + 4. First run the python server provided. This ensures that the code is + executed in a safe environment. Do this like so:: + + $ sudo python testapp/code_server.py + + Put this in the background once it has started since this will not + return back the prompt. It is important that the server be running + *before* students start attempting the exam. Using sudo is + necessary since the server is run as the user "nobody". This runs + on the ports configured in the settings.py file in the variable + "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. + This is the maximum time allowed to execute the submitted code. + Note that this will likely spawn multiple processes as "nobody" + depending on the number of server ports specified. + + 5. The ``bin/django.wsgi`` script should make it + easy to deploy this using mod_wsgi. You will need to add a line of the form: + + WSGIScriptAlias / "/var/www/online_test/bin/django.wsgi" + + to your apache.conf. For more details see the Django docs here: - 2. Run:: + https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ + + 6. Go to http://deserved_host_or_ip:desired_port/admin - $ python manage.py syncdb - [ enter password etc.] + 7. Login with your credentials and look at the questions and modify if + needed. Create a new Quiz, set the date and duration or + activate/deactivate the quiz. - $ python manage.py migrate exam + 8. Now ask users to login at: - 3. Add questions by editing the "docs/sample_questions.py" or any other + http://host:port/exam + + And you should be all set. + + 9. Note that the directory "output" will contain directories, one for each + user. Users can potentially write output into these that can be used + for checking later. + + 10. As admin user you can visit http://host/exam/monitor to view + results and user data interactively. You could also "grade" the + papers manually if needed. + + 11. You may dump the results and user data using the results2csv and + dump_user_data commands. + +Development Settings +==================== + +To install/deploy this app follow the steps below: + + 1. Clone this repository and cd to the cloned repo. + + 2. run python bootstrap.py + + 3. run ./bin/buildout -c buildout.cfg + + 4. run ./bin/django syncdb + [ enter password etc.] + + run ./bin/django migrate exam + + 5. Add questions by editing the "docs/sample_questions.py" or any other file in the same format and then run the following:: - $ python manage.py load_exam docs/sample_questions.py + ./bin/django load_exam docs/sample_questions.py Note that you can supply multiple Python files as arguments and all of those will be added to the database. @@ -66,7 +145,7 @@ To install/deploy this app follow the steps below: 4. First run the python server provided. This ensures that the code is executed in a safe environment. Do this like so:: - $ sudo python code_server.py + $ sudo python testapp/code_server.py Put this in the background once it has started since this will not return back the prompt. It is important that the server be running @@ -77,14 +156,11 @@ To install/deploy this app follow the steps below: This is the maximum time allowed to execute the submitted code. Note that this will likely spawn multiple processes as "nobody" depending on the number of server ports specified. - + 5. Now, run:: - $ python manage.py runserver : - - For deployment use Apache or a real webserver, see below for more - information. - + $ ./bin/django runserver : + 6. Go to http://deserved_host_or_ip:desired_port/admin 7. Login with your credentials and look at the questions and modify if @@ -107,10 +183,7 @@ To install/deploy this app follow the steps below: 11. You may dump the results and user data using the results2csv and dump_user_data commands. - -WARNING: django is running in debug mode for this currently, CHANGE it -during deployment. To do this, edit settings.py and set DEBUG to False. -Also look at other settings and change them suitably. + The file docs/sample_questions.py is a template that you can use for your own questions. @@ -139,27 +212,6 @@ For more information on these do this:: where [command] is one of the above. -Deploying via Apache -===================== - -For any serious deployment, you will need to deploy the app using a real -webserver like Apache. The ``apache/django.wsgi`` script should make it -easy to deploy this using mod_wsgi. You will need to add a line of the -form: - - WSGIScriptAlias / "/var/www/online_test/apache/django.wsgi" - -to your apache.conf. For more details see the Django docs here: - -https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ - - -Sometimes you might be in the situation where you are not hosted as -"host.org/exam/" but as "host.org/foo/exam/" for whatever reason. In -this case edit "settings.py" and set the "URL_ROOT" to the root you -have to serve at. In the above example for "host.org/foo/exam" set -URL_ROOT='/foo'. - License ======= @@ -186,5 +238,4 @@ I gratefully acknowledge help from the following: the code. -Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) - +Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) \ No newline at end of file -- cgit From b49fbfe2f3438f6d59af36de416cf9b7f33723ab Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:20:46 +0530 Subject: Changed README.txt to README.rst --- README.rst | 241 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.txt | 241 ------------------------------------------------------------- 2 files changed, 241 insertions(+), 241 deletions(-) create mode 100644 README.rst delete mode 100644 README.txt diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..75d3061 --- /dev/null +++ b/README.rst @@ -0,0 +1,241 @@ +Introduction +============ + +This app provides an "exam" app that lets users take an online +programming quiz. Currently only Python and simple Bash scripts can be +tested. At FOSSEE, Nishanth had implemented a nice django based app to +test for multiple-choice questions. However, I was inspired by a +programming contest that I saw at PyCon APAC 2011. Chris Boesch, who +administered the contest, used a nice web application that he had built +on top of GAE that basically checked your Python code, live. This made +it fun and interesting. Their application can be seen at +http://singpath.com + +I wanted an implementation that was not tied to GAE and decided to write +one myself and the result is the "exam" app. The idea being that I can +use this to test students programming skills and not have to worry about +grading their answers myself and I can do so on my machines. + +You can define fairly complicated programming problems and have users +solve the problem and the solution is checked immediately. The system +supports pretty much arbitrary Python and uses "test cases" to test the +implementations of the students. It also supports simple bash scripts +-- see the sample questions in "docs/". In addition it supports simple +multiple choice questions. Since it runs on your Python, you could +technically test any Python based library. It is distributed under the +BSD license. + +It can use a lot more work but the basics work and the app scales to +over 500+ simultaneous users. :) + +Pre-Requisite +============= + + 1. Install MySql Server + + 2. Install Python MySql support + + 3. Install Apache Server for deployment + +Configure MySql server +---------------------- + + 1. Create a database named `online_test` + + 2. Add a user named `online_test_user` and give access to it on the database `online_test` + + 3. Create a file named `local.py` in folder `testapp` and insert `DATABASE_PASSWORD = 'yourpassword'` + + +Production Deployment +===================== + +To install/deploy this app follow the steps below: + + 1. Clone this repository and cd to the cloned repo. + + 2. run python bootstrap.py + + 3. run ./bin/buildout -c production.cfg + + 4. run ./bin/django syncdb + [ enter password etc.] + + run ./bin/django migrate exam + + 5. Add questions by editing the "docs/sample_questions.py" or any other + file in the same format and then run the following:: + + ./bin/django load_exam docs/sample_questions.py + + Note that you can supply multiple Python files as arguments and all of + those will be added to the database. + + 4. First run the python server provided. This ensures that the code is + executed in a safe environment. Do this like so:: + + $ sudo python testapp/code_server.py + + Put this in the background once it has started since this will not + return back the prompt. It is important that the server be running + *before* students start attempting the exam. Using sudo is + necessary since the server is run as the user "nobody". This runs + on the ports configured in the settings.py file in the variable + "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. + This is the maximum time allowed to execute the submitted code. + Note that this will likely spawn multiple processes as "nobody" + depending on the number of server ports specified. + + 5. The ``bin/django.wsgi`` script should make it + easy to deploy this using mod_wsgi. You will need to add a line of the form: + + WSGIScriptAlias / "/var/www/online_test/bin/django.wsgi" + + to your apache.conf. For more details see the Django docs here: + + https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ + + 6. Go to http://deserved_host_or_ip:desired_port/admin + + 7. Login with your credentials and look at the questions and modify if + needed. Create a new Quiz, set the date and duration or + activate/deactivate the quiz. + + 8. Now ask users to login at: + + http://host:port/exam + + And you should be all set. + + 9. Note that the directory "output" will contain directories, one for each + user. Users can potentially write output into these that can be used + for checking later. + + 10. As admin user you can visit http://host/exam/monitor to view + results and user data interactively. You could also "grade" the + papers manually if needed. + + 11. You may dump the results and user data using the results2csv and + dump_user_data commands. + +Development Settings +==================== + +To install/deploy this app follow the steps below: + + 1. Clone this repository and cd to the cloned repo. + + 2. run python bootstrap.py + + 3. run ./bin/buildout -c buildout.cfg + + 4. run ./bin/django syncdb + [ enter password etc.] + + run ./bin/django migrate exam + + 5. Add questions by editing the "docs/sample_questions.py" or any other + file in the same format and then run the following:: + + ./bin/django load_exam docs/sample_questions.py + + Note that you can supply multiple Python files as arguments and all of + those will be added to the database. + + 4. First run the python server provided. This ensures that the code is + executed in a safe environment. Do this like so:: + + $ sudo python testapp/code_server.py + + Put this in the background once it has started since this will not + return back the prompt. It is important that the server be running + *before* students start attempting the exam. Using sudo is + necessary since the server is run as the user "nobody". This runs + on the ports configured in the settings.py file in the variable + "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. + This is the maximum time allowed to execute the submitted code. + Note that this will likely spawn multiple processes as "nobody" + depending on the number of server ports specified. + + 5. Now, run:: + + $ ./bin/django runserver : + + 6. Go to http://deserved_host_or_ip:desired_port/admin + + 7. Login with your credentials and look at the questions and modify if + needed. Create a new Quiz, set the date and duration or + activate/deactivate the quiz. + + 8. Now ask users to login at: + + http://host:port/exam + + And you should be all set. + + 9. Note that the directory "output" will contain directories, one for each + user. Users can potentially write output into these that can be used + for checking later. + + 10. As admin user you can visit http://host/exam/monitor to view + results and user data interactively. You could also "grade" the + papers manually if needed. + + 11. You may dump the results and user data using the results2csv and + dump_user_data commands. + + +The file docs/sample_questions.py is a template that you can use for your +own questions. + +Additional commands available +============================== + +We provide several convenient commands for you to use: + + - load_exam : load questions and a quiz from a python file. See + docs/sample_questions.py + + - load_questions_xml : load questions from XML file, see + docs/sample_questions.xml use of this is deprecated in favor of + load_exam. + + - results2csv : Dump the quiz results into a CSV file for further + processing. + + - dump_user_data : Dump out relevalt user data for either all users or + specified users. + +For more information on these do this:: + + $ ./manage.py help [command] + +where [command] is one of the above. + +License +======= + +This is distributed under the terms of the BSD license. Copyright +information is at the bottom of this file. + +Authors +======= + +Main author: Prabhu Ramachandran + +I gratefully acknowledge help from the following: + + - Nishanth Amuluru originally from FOSSEE who wrote bulk of the + login/registration code. He wrote an initial first cut of a quiz app + which supported only simple questions which provided motivation for + this app. The current codebase does not share too much from his + implementation although there are plenty of similarities. + + - Harish Badrinath (FOSSEE) -- who provided a first cut of the bash + related scripts. + + - Srikant Patnaik and Thomas Stephen Lee, who helped deploy and test + the code. + + +Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) \ No newline at end of file diff --git a/README.txt b/README.txt deleted file mode 100644 index 75d3061..0000000 --- a/README.txt +++ /dev/null @@ -1,241 +0,0 @@ -Introduction -============ - -This app provides an "exam" app that lets users take an online -programming quiz. Currently only Python and simple Bash scripts can be -tested. At FOSSEE, Nishanth had implemented a nice django based app to -test for multiple-choice questions. However, I was inspired by a -programming contest that I saw at PyCon APAC 2011. Chris Boesch, who -administered the contest, used a nice web application that he had built -on top of GAE that basically checked your Python code, live. This made -it fun and interesting. Their application can be seen at -http://singpath.com - -I wanted an implementation that was not tied to GAE and decided to write -one myself and the result is the "exam" app. The idea being that I can -use this to test students programming skills and not have to worry about -grading their answers myself and I can do so on my machines. - -You can define fairly complicated programming problems and have users -solve the problem and the solution is checked immediately. The system -supports pretty much arbitrary Python and uses "test cases" to test the -implementations of the students. It also supports simple bash scripts --- see the sample questions in "docs/". In addition it supports simple -multiple choice questions. Since it runs on your Python, you could -technically test any Python based library. It is distributed under the -BSD license. - -It can use a lot more work but the basics work and the app scales to -over 500+ simultaneous users. :) - -Pre-Requisite -============= - - 1. Install MySql Server - - 2. Install Python MySql support - - 3. Install Apache Server for deployment - -Configure MySql server ----------------------- - - 1. Create a database named `online_test` - - 2. Add a user named `online_test_user` and give access to it on the database `online_test` - - 3. Create a file named `local.py` in folder `testapp` and insert `DATABASE_PASSWORD = 'yourpassword'` - - -Production Deployment -===================== - -To install/deploy this app follow the steps below: - - 1. Clone this repository and cd to the cloned repo. - - 2. run python bootstrap.py - - 3. run ./bin/buildout -c production.cfg - - 4. run ./bin/django syncdb - [ enter password etc.] - - run ./bin/django migrate exam - - 5. Add questions by editing the "docs/sample_questions.py" or any other - file in the same format and then run the following:: - - ./bin/django load_exam docs/sample_questions.py - - Note that you can supply multiple Python files as arguments and all of - those will be added to the database. - - 4. First run the python server provided. This ensures that the code is - executed in a safe environment. Do this like so:: - - $ sudo python testapp/code_server.py - - Put this in the background once it has started since this will not - return back the prompt. It is important that the server be running - *before* students start attempting the exam. Using sudo is - necessary since the server is run as the user "nobody". This runs - on the ports configured in the settings.py file in the variable - "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. - This is the maximum time allowed to execute the submitted code. - Note that this will likely spawn multiple processes as "nobody" - depending on the number of server ports specified. - - 5. The ``bin/django.wsgi`` script should make it - easy to deploy this using mod_wsgi. You will need to add a line of the form: - - WSGIScriptAlias / "/var/www/online_test/bin/django.wsgi" - - to your apache.conf. For more details see the Django docs here: - - https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ - - 6. Go to http://deserved_host_or_ip:desired_port/admin - - 7. Login with your credentials and look at the questions and modify if - needed. Create a new Quiz, set the date and duration or - activate/deactivate the quiz. - - 8. Now ask users to login at: - - http://host:port/exam - - And you should be all set. - - 9. Note that the directory "output" will contain directories, one for each - user. Users can potentially write output into these that can be used - for checking later. - - 10. As admin user you can visit http://host/exam/monitor to view - results and user data interactively. You could also "grade" the - papers manually if needed. - - 11. You may dump the results and user data using the results2csv and - dump_user_data commands. - -Development Settings -==================== - -To install/deploy this app follow the steps below: - - 1. Clone this repository and cd to the cloned repo. - - 2. run python bootstrap.py - - 3. run ./bin/buildout -c buildout.cfg - - 4. run ./bin/django syncdb - [ enter password etc.] - - run ./bin/django migrate exam - - 5. Add questions by editing the "docs/sample_questions.py" or any other - file in the same format and then run the following:: - - ./bin/django load_exam docs/sample_questions.py - - Note that you can supply multiple Python files as arguments and all of - those will be added to the database. - - 4. First run the python server provided. This ensures that the code is - executed in a safe environment. Do this like so:: - - $ sudo python testapp/code_server.py - - Put this in the background once it has started since this will not - return back the prompt. It is important that the server be running - *before* students start attempting the exam. Using sudo is - necessary since the server is run as the user "nobody". This runs - on the ports configured in the settings.py file in the variable - "SERVER_PORTS". The "SERVER_TIMEOUT" also can be changed there. - This is the maximum time allowed to execute the submitted code. - Note that this will likely spawn multiple processes as "nobody" - depending on the number of server ports specified. - - 5. Now, run:: - - $ ./bin/django runserver : - - 6. Go to http://deserved_host_or_ip:desired_port/admin - - 7. Login with your credentials and look at the questions and modify if - needed. Create a new Quiz, set the date and duration or - activate/deactivate the quiz. - - 8. Now ask users to login at: - - http://host:port/exam - - And you should be all set. - - 9. Note that the directory "output" will contain directories, one for each - user. Users can potentially write output into these that can be used - for checking later. - - 10. As admin user you can visit http://host/exam/monitor to view - results and user data interactively. You could also "grade" the - papers manually if needed. - - 11. You may dump the results and user data using the results2csv and - dump_user_data commands. - - -The file docs/sample_questions.py is a template that you can use for your -own questions. - -Additional commands available -============================== - -We provide several convenient commands for you to use: - - - load_exam : load questions and a quiz from a python file. See - docs/sample_questions.py - - - load_questions_xml : load questions from XML file, see - docs/sample_questions.xml use of this is deprecated in favor of - load_exam. - - - results2csv : Dump the quiz results into a CSV file for further - processing. - - - dump_user_data : Dump out relevalt user data for either all users or - specified users. - -For more information on these do this:: - - $ ./manage.py help [command] - -where [command] is one of the above. - -License -======= - -This is distributed under the terms of the BSD license. Copyright -information is at the bottom of this file. - -Authors -======= - -Main author: Prabhu Ramachandran - -I gratefully acknowledge help from the following: - - - Nishanth Amuluru originally from FOSSEE who wrote bulk of the - login/registration code. He wrote an initial first cut of a quiz app - which supported only simple questions which provided motivation for - this app. The current codebase does not share too much from his - implementation although there are plenty of similarities. - - - Harish Badrinath (FOSSEE) -- who provided a first cut of the bash - related scripts. - - - Srikant Patnaik and Thomas Stephen Lee, who helped deploy and test - the code. - - -Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) \ No newline at end of file -- cgit From 5c20e867622cd72debae13cdbd1240b0c5588e5a Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:27:57 +0530 Subject: made some modifications to the readme file --- README.rst | 108 ++++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/README.rst b/README.rst index 75d3061..784962b 100644 --- a/README.rst +++ b/README.rst @@ -31,48 +31,55 @@ over 500+ simultaneous users. :) Pre-Requisite ============= - 1. Install MySql Server + #. Install MySql Server - 2. Install Python MySql support + #. Install Python MySql support - 3. Install Apache Server for deployment + #. Install Apache Server for deployment Configure MySql server ---------------------- - 1. Create a database named `online_test` + #. Create a database named ``online_test`` - 2. Add a user named `online_test_user` and give access to it on the database `online_test` + #. Add a user named ``online_test_user`` and give access to it on the database ``online_test`` - 3. Create a file named `local.py` in folder `testapp` and insert `DATABASE_PASSWORD = 'yourpassword'` + #. Create a file named `local.py` in folder `testapp` and insert `DATABASE_PASSWORD = 'yourpassword'` Production Deployment ===================== -To install/deploy this app follow the steps below: +To deploy this app follow the steps below: - 1. Clone this repository and cd to the cloned repo. + #. Clone this repository and cd to the cloned repo. - 2. run python bootstrap.py + #. run:: - 3. run ./bin/buildout -c production.cfg + python bootstrap.py - 4. run ./bin/django syncdb - [ enter password etc.] + #. run:: - run ./bin/django migrate exam + ./bin/buildout -c production.cfg + + #. run:: + + ./bin/django syncdb + + [ enter password etc.] + + run:: - 5. Add questions by editing the "docs/sample_questions.py" or any other - file in the same format and then run the following:: + ./bin/django migrate exam + + #. Add questions by editing the "docs/sample_questions.py" or any other file in the same format and then run the following:: ./bin/django load_exam docs/sample_questions.py Note that you can supply multiple Python files as arguments and all of those will be added to the database. - 4. First run the python server provided. This ensures that the code is - executed in a safe environment. Do this like so:: + #. First run the python server provided. This ensures that the code is executed in a safe environment. Do this like so:: $ sudo python testapp/code_server.py @@ -86,7 +93,7 @@ To install/deploy this app follow the steps below: Note that this will likely spawn multiple processes as "nobody" depending on the number of server ports specified. - 5. The ``bin/django.wsgi`` script should make it + #. The ``bin/django.wsgi`` script should make it easy to deploy this using mod_wsgi. You will need to add a line of the form: WSGIScriptAlias / "/var/www/online_test/bin/django.wsgi" @@ -95,55 +102,62 @@ To install/deploy this app follow the steps below: https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ - 6. Go to http://deserved_host_or_ip:desired_port/admin + #. Go to http://deserved_host_or_ip:desired_port/admin - 7. Login with your credentials and look at the questions and modify if + #. Login with your credentials and look at the questions and modify if needed. Create a new Quiz, set the date and duration or activate/deactivate the quiz. - 8. Now ask users to login at: + #. Now ask users to login at: http://host:port/exam And you should be all set. - 9. Note that the directory "output" will contain directories, one for each + #. Note that the directory "output" will contain directories, one for each user. Users can potentially write output into these that can be used for checking later. - 10. As admin user you can visit http://host/exam/monitor to view + #. As admin user you can visit http://host/exam/monitor to view results and user data interactively. You could also "grade" the papers manually if needed. - 11. You may dump the results and user data using the results2csv and + #. You may dump the results and user data using the results2csv and dump_user_data commands. Development Settings ==================== -To install/deploy this app follow the steps below: +To install this app follow the steps below: - 1. Clone this repository and cd to the cloned repo. + #. Clone this repository and cd to the cloned repo. + + #. run:: + + python bootstrap.py - 2. run python bootstrap.py + #. run:: - 3. run ./bin/buildout -c buildout.cfg + ./bin/buildout -c production.cfg - 4. run ./bin/django syncdb - [ enter password etc.] + #. run:: - run ./bin/django migrate exam + ./bin/django syncdb + + [ enter password etc.] + + run:: + + ./bin/django migrate exam - 5. Add questions by editing the "docs/sample_questions.py" or any other - file in the same format and then run the following:: + #. Add questions by editing the "docs/sample_questions.py" or any other file in the same format and then run the following:: ./bin/django load_exam docs/sample_questions.py Note that you can supply multiple Python files as arguments and all of those will be added to the database. - 4. First run the python server provided. This ensures that the code is - executed in a safe environment. Do this like so:: + #. First run the python server provided. This ensures that the code is executed in a safe environment. Do this like so:: $ sudo python testapp/code_server.py @@ -157,35 +171,35 @@ To install/deploy this app follow the steps below: Note that this will likely spawn multiple processes as "nobody" depending on the number of server ports specified. - 5. Now, run:: - - $ ./bin/django runserver : + #. Now, run:: + + $ ./bin/django runserver : - 6. Go to http://deserved_host_or_ip:desired_port/admin + #. Go to http://deserved_host_or_ip:desired_port/admin - 7. Login with your credentials and look at the questions and modify if + #. Login with your credentials and look at the questions and modify if needed. Create a new Quiz, set the date and duration or activate/deactivate the quiz. - 8. Now ask users to login at: + #. Now ask users to login at: http://host:port/exam And you should be all set. - 9. Note that the directory "output" will contain directories, one for each + #. Note that the directory "output" will contain directories, one for each user. Users can potentially write output into these that can be used for checking later. - 10. As admin user you can visit http://host/exam/monitor to view + #. As admin user you can visit http://host/exam/monitor to view results and user data interactively. You could also "grade" the papers manually if needed. - 11. You may dump the results and user data using the results2csv and + #. You may dump the results and user data using the results2csv and dump_user_data commands. - - -The file docs/sample_questions.py is a template that you can use for your + + + The file docs/sample_questions.py is a template that you can use for your own questions. Additional commands available -- cgit From bfeb9fd61fb2eb1f8483466270295234b8f17421 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:30:49 +0530 Subject: made some modifications to the readme file --- README.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 784962b..7286bb1 100644 --- a/README.rst +++ b/README.rst @@ -60,13 +60,11 @@ To deploy this app follow the steps below: #. run:: - ./bin/buildout -c production.cfg + ./bin/buildout -c production.cfg #. run:: - ./bin/django syncdb - - [ enter password etc.] + ./bin/django syncdb run:: -- cgit From 54235fd3b2dd188a386056d5811a5370cf8c79fb Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:31:57 +0530 Subject: made some modifications to the readme file --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7286bb1..466f148 100644 --- a/README.rst +++ b/README.rst @@ -140,9 +140,7 @@ To install this app follow the steps below: #. run:: - ./bin/django syncdb - - [ enter password etc.] + ./bin/django syncdb run:: -- cgit From 9c10310bb2e623b3b8b3f13b0d162e2009ad8239 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:33:37 +0530 Subject: made some modifications to the readme file --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 466f148..dc33a1f 100644 --- a/README.rst +++ b/README.rst @@ -195,8 +195,7 @@ To install this app follow the steps below: dump_user_data commands. - The file docs/sample_questions.py is a template that you can use for your -own questions. + The file docs/sample_questions.py is a template that you can use for your own questions. Additional commands available ============================== -- cgit From 364650e602d751d920b8579fb10546a55ed13d38 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:34:17 +0530 Subject: made some modifications to the readme file --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index dc33a1f..64113da 100644 --- a/README.rst +++ b/README.rst @@ -128,7 +128,7 @@ Development Settings To install this app follow the steps below: - #. Clone this repository and cd to the cloned repo. + #. Clone this repository and cd to the cloned repo. #. run:: -- cgit From 4a2cb590dc422345f22e0ad1435ab954d77458a3 Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 9 Dec 2011 05:42:54 +0530 Subject: made some modifications to the readme file --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 64113da..29fcdc3 100644 --- a/README.rst +++ b/README.rst @@ -56,7 +56,7 @@ To deploy this app follow the steps below: #. run:: - python bootstrap.py + python bootstrap.py #. run:: @@ -74,8 +74,8 @@ To deploy this app follow the steps below: ./bin/django load_exam docs/sample_questions.py - Note that you can supply multiple Python files as arguments and all of - those will be added to the database. + Note that you can supply multiple Python files as arguments and all of + those will be added to the database. #. First run the python server provided. This ensures that the code is executed in a safe environment. Do this like so:: @@ -132,11 +132,11 @@ To install this app follow the steps below: #. run:: - python bootstrap.py + python bootstrap.py #. run:: - ./bin/buildout -c production.cfg + ./bin/buildout -c production.cfg #. run:: @@ -150,8 +150,8 @@ To install this app follow the steps below: ./bin/django load_exam docs/sample_questions.py - Note that you can supply multiple Python files as arguments and all of - those will be added to the database. + Note that you can supply multiple Python files as arguments and all of + those will be added to the database. #. First run the python server provided. This ensures that the code is executed in a safe environment. Do this like so:: -- cgit From e43c58cd5baf03a4fb2052f20c867aec5fbf849f Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 23 Dec 2011 00:57:55 +0530 Subject: Made changes to README to include detailed instructions for mysql installation and URL_ROOT as per Prof. Prabhu Ramachandran's sugestions --- README.rst | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 29fcdc3..72463c3 100644 --- a/README.rst +++ b/README.rst @@ -27,9 +27,13 @@ BSD license. It can use a lot more work but the basics work and the app scales to over 500+ simultaneous users. :) + + +Production Deployment +===================== Pre-Requisite -============= +------------- #. Install MySql Server @@ -40,15 +44,19 @@ Pre-Requisite Configure MySql server ---------------------- - #. Create a database named ``online_test`` + #. Create a database named ``online_test`` by following the steps below + + #. $> mysql -u root -p + + #. mysql> create database online_test - #. Add a user named ``online_test_user`` and give access to it on the database ``online_test`` + #. Add a user named ``online_test_user`` and give access to it on the database ``online_test`` by following the steps below - #. Create a file named `local.py` in folder `testapp` and insert `DATABASE_PASSWORD = 'yourpassword'` - - -Production Deployment -===================== + #. mysql> grant usage on online_test.* to online_test_user@localhost identified by 'mysecretpassword'; + + #. mysql> grant all privileges on online_test.* to online_test_user@localhost; + + #. Create a file named `local.py` in folder `testapp` and insert `DATABASE_PASSWORD = 'mysecretpassword'` To deploy this app follow the steps below: @@ -100,7 +108,7 @@ To deploy this app follow the steps below: https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/ - #. Go to http://deserved_host_or_ip:desired_port/admin + #. Go to http://desired_host_or_ip:desired_port/admin #. Login with your credentials and look at the questions and modify if needed. Create a new Quiz, set the date and duration or @@ -122,6 +130,14 @@ To deploy this app follow the steps below: #. You may dump the results and user data using the results2csv and dump_user_data commands. + + #. The file docs/sample_questions.py is a template that you can use for your own questions. + + #. Sometimes you might be in the situation where you are not hosted as + "host.org/exam/" but as "host.org/foo/exam/" for whatever reason. In + this case edit "settings.py" and set the "URL_ROOT" to the root you + have to serve at. In the above example for "host.org/foo/exam" set + URL_ROOT='/foo'. Development Settings ==================== @@ -194,8 +210,13 @@ To install this app follow the steps below: #. You may dump the results and user data using the results2csv and dump_user_data commands. + #. The file docs/sample_questions.py is a template that you can use for your own questions. - The file docs/sample_questions.py is a template that you can use for your own questions. + #. Sometimes you might be in the situation where you are not hosted as + "host.org/exam/" but as "host.org/foo/exam/" for whatever reason. In + this case edit "settings.py" and set the "URL_ROOT" to the root you + have to serve at. In the above example for "host.org/foo/exam" set + URL_ROOT='/foo'. Additional commands available ============================== @@ -247,4 +268,4 @@ I gratefully acknowledge help from the following: the code. -Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) \ No newline at end of file +Copyright (c) 2011 Prabhu Ramachandran and FOSSEE (fossee.in) -- cgit From 7b819758d4d60822c19611845a44f8c5301a391c Mon Sep 17 00:00:00 2001 From: parth Date: Fri, 23 Dec 2011 00:58:43 +0530 Subject: Removed PIL from buildout as it was not currently required --- buildout.cfg | 1 - production.cfg | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/buildout.cfg b/buildout.cfg index c4a76b4..c04ec59 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -2,7 +2,6 @@ parts = django eggs = - PIL South [versions] diff --git a/production.cfg b/production.cfg index e3ee886..9ea9f7b 100644 --- a/production.cfg +++ b/production.cfg @@ -1,8 +1,7 @@ [buildout] parts = django -eggs = - PIL +eggs = South MySQL-python -- cgit