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 --- 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 ++ 42 files changed, 2760 insertions(+) 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 (limited to 'testapp') 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)), +) -- 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 (limited to 'testapp') 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 --- testapp/README.txt | 190 ----------------------------------------------------- 1 file changed, 190 deletions(-) delete mode 100644 testapp/README.txt (limited to 'testapp') 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 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 --- testapp/production.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 testapp/production.py (limited to '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