summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPrabhu Ramachandran2011-11-08 02:10:03 +0530
committerPrabhu Ramachandran2011-11-08 02:10:03 +0530
commitec738bb79132a581a1778187a55632983d1fcb53 (patch)
treed9491ee26367bb504143f5b5bd3a392160c2dbbb
downloadonline_test-ec738bb79132a581a1778187a55632983d1fcb53.tar.gz
online_test-ec738bb79132a581a1778187a55632983d1fcb53.tar.bz2
online_test-ec738bb79132a581a1778187a55632983d1fcb53.zip
NEW: First cut of exam app.
This application allows us to create questions via the admin interface. The questions are proper programming questions and one enters test cases for each question. The user logs in and submits the answer which is checked. The app is rather simple now but does work.
-rw-r--r--__init__.py0
-rw-r--r--exam/__init__.py0
-rw-r--r--exam/admin.py4
-rw-r--r--exam/forms.py15
-rw-r--r--exam/models.py102
-rw-r--r--exam/tests.py16
-rw-r--r--exam/urls.py9
-rw-r--r--exam/views.py150
-rwxr-xr-xmanage.py14
-rw-r--r--settings.py153
-rw-r--r--templates/exam/complete.html3
-rw-r--r--templates/exam/index.html12
-rw-r--r--templates/exam/question.html20
-rw-r--r--templates/exam/register.html7
-rw-r--r--urls.py14
15 files changed, 519 insertions, 0 deletions
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/__init__.py
diff --git a/exam/__init__.py b/exam/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/exam/__init__.py
diff --git a/exam/admin.py b/exam/admin.py
new file mode 100644
index 0000000..d40a7d6
--- /dev/null
+++ b/exam/admin.py
@@ -0,0 +1,4 @@
+from exam.models import Question
+from django.contrib import admin
+
+admin.site.register(Question) \ No newline at end of file
diff --git a/exam/forms.py b/exam/forms.py
new file mode 100644
index 0000000..bd5732e
--- /dev/null
+++ b/exam/forms.py
@@ -0,0 +1,15 @@
+from django import forms
+from exam.models import Profile
+
+class UserRegisterForm(forms.ModelForm):
+
+ first_name = forms.CharField(max_length=30)
+ last_name = forms.CharField(max_length=30)
+ roll_number = forms.CharField(max_length=30)
+ #email_address = forms.EmailField()
+ #password = forms.CharField(max_length=30, widget=forms.PasswordInput())
+
+ class Meta:
+ model = Profile
+ fields = ['first_name', 'last_name', 'roll_number']
+
diff --git a/exam/models.py b/exam/models.py
new file mode 100644
index 0000000..247b362
--- /dev/null
+++ b/exam/models.py
@@ -0,0 +1,102 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+# Create your models here.
+
+class Profile(models.Model):
+ """Profile for a user to store roll number etc."""
+ user = models.ForeignKey(User)
+ roll_number = models.CharField(max_length=20)
+
+class Question(models.Model):
+ """A question in the database."""
+ # An optional one-line summary of the question.
+ summary = models.CharField(max_length=256)
+ # The question text.
+ question = models.TextField()
+
+ # Number of points for the question.
+ points = models.IntegerField()
+
+ # Test cases for the question in the form of code that is run.
+ # This is simple Python code.
+ test = models.TextField()
+
+ def __unicode__(self):
+ return self.summary
+
+
+class Answer(models.Model):
+ """Answers submitted by users.
+ """
+ question = models.ForeignKey(Question)
+ # The last answer submitted by the user.
+ answer = models.TextField()
+ attempts = models.IntegerField()
+
+ # Is the question correct.
+ correct = models.BooleanField()
+ # Marks obtained.
+ marks = models.IntegerField()
+
+ def __unicode__(self):
+ return self.answer
+
+class Quiz(models.Model):
+ """A quiz for a student.
+ """
+ user = models.ForeignKey(User)
+ user_ip = models.CharField(max_length=15)
+ key = models.CharField(max_length=10)
+ questions = models.CharField(max_length=128)
+ questions_answered = models.CharField(max_length=128)
+
+ 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 answered_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 __unicode__(self):
+ u = self.user
+ return u'Quiz for {0} {1}'.format(u.first_name, u.last_name)
diff --git a/exam/tests.py b/exam/tests.py
new file mode 100644
index 0000000..501deb7
--- /dev/null
+++ b/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/exam/urls.py b/exam/urls.py
new file mode 100644
index 0000000..7922255
--- /dev/null
+++ b/exam/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import patterns, include, url
+
+urlpatterns = patterns('exam.views',
+ url(r'^$', 'index'),
+ url(r'^start/$', 'start'),
+ url(r'^complete/$', 'complete'),
+ url(r'^(?P<q_id>\d+)/$', 'question'),
+ url(r'^(?P<q_id>\d+)/check/$', 'check'),
+)
diff --git a/exam/views.py b/exam/views.py
new file mode 100644
index 0000000..10d6c9a
--- /dev/null
+++ b/exam/views.py
@@ -0,0 +1,150 @@
+import random
+import sys
+import traceback
+import string
+
+from django.db import IntegrityError
+from django.contrib.auth.models import User
+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 exam.models import Question, Quiz, Profile
+from exam.forms import UserRegisterForm
+
+def gen_key(no_of_chars):
+ allowed_chars = string.digits+string.uppercase
+ return ''.join([random.choice(allowed_chars) for i in range(no_of_chars)])
+
+def index_old(request):
+ """The start page.
+ """
+ question_list = Question.objects.all()
+ context = {'question_list': question_list}
+ return render_to_response('exam/index.html', context)
+
+def index(request):
+ """The start page.
+ """
+ # Largely copied from Nishanth's quiz app.
+ user = request.user
+ if user.is_authenticated():
+ return redirect("/exam/start/")
+ else:
+ try:
+ ip = request.META['REMOTE_ADDR']
+ Quiz.objects.get(user_ip=ip)
+ return redirect("/exam/complete")
+ except Quiz.DoesNotExist:
+ pass
+
+ if request.method == "POST":
+ form = UserRegisterForm(request.POST)
+ if form.is_valid():
+ data = form.cleaned_data
+
+ while True:
+ try:
+ username = gen_key(20)
+ new_user = User.objects.create_user(username, "temp@temp.com", "123")
+ break
+ except IntegrityError:
+ pass
+
+ new_user.first_name = data['first_name']
+ new_user.last_name = data['last_name']
+ new_user.save()
+
+ new_profile = Profile(user=new_user)
+ new_profile.roll_number = data['roll_number']
+ new_profile.save()
+
+ user = authenticate(username=username, password="123")
+ login(request, user)
+ return redirect("/exam/start/")
+
+ else:
+ return render_to_response('exam/register.html',{'form':form},
+ context_instance=RequestContext(request))
+ else:
+ form = UserRegisterForm()
+ return render_to_response('exam/register.html',{'form':form},
+ context_instance=RequestContext(request))
+
+def show_question(request, q_id):
+ if len(q_id) == 0:
+ return redirect("/exam/complete")
+ else:
+ return question(request, q_id)
+
+def start(request):
+ user = request.user
+ try:
+ old_quiz = Quiz.objects.get(user=user)
+ q = old_quiz.current_question()
+ return show_question(request, q)
+ except Quiz.DoesNotExist:
+ ip = request.META['REMOTE_ADDR']
+ key = gen_key(10)
+ new_quiz = Quiz(user=user, user_ip=ip, key=key)
+
+ questions = [ str(_.id) for _ in Question.objects.all() ]
+ random.shuffle(questions)
+ questions = questions[:3]
+
+ new_quiz.questions = "|".join(questions)
+ new_quiz.save()
+ q = new_quiz.current_question()
+
+ return show_question(request, q)
+
+def question(request, q_id):
+ q = get_object_or_404(Question, pk=q_id)
+ try:
+ quiz = Quiz.objects.get(user=request.user)
+ except Quiz.DoesNotExist:
+ redirect('/exam/start')
+ context = {'question': q, 'quiz': quiz}
+ ci = RequestContext(request)
+ return render_to_response('exam/question.html', context,
+ context_instance=ci)
+
+def test_answer(func_code, test_code):
+ exec func_code
+ exec test_code
+
+def check(request, q_id):
+ user = request.user
+ question = get_object_or_404(Question, pk=q_id)
+ quiz = Quiz.objects.get(user=user)
+ answer = request.POST.get('answer')
+ skip = request.POST.get('skip', None)
+
+ if skip is not None:
+ next_q = quiz.skip()
+ return show_question(request, next_q)
+
+ # Otherwise we were asked to check.
+ retry = True
+ try:
+ test_answer(answer, question.test)
+ except:
+ type, value, tb = sys.exc_info()
+ info = traceback.extract_tb(tb)
+ fname, lineno, func, text = info[-1]
+ err = "{0}: {1} In code: {2}".format(type.__name__, str(value), text)
+ else:
+ retry = False
+ err = 'Correct answer'
+
+ ci = RequestContext(request)
+ if retry:
+ context = {'question': question, 'error_message': err}
+ return render_to_response('exam/question.html', context,
+ context_instance=ci)
+ else:
+ next_q = quiz.answered_question(question.id)
+ return show_question(request, next_q)
+
+def complete(request):
+ return render_to_response('exam/complete.html')
+ \ No newline at end of file
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..3e4eedc
--- /dev/null
+++ b/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/settings.py b/settings.py
new file mode 100644
index 0000000..3678399
--- /dev/null
+++ b/settings.py
@@ -0,0 +1,153 @@
+# Django settings for tester project.
+
+from os.path import dirname, join, expanduser
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@example.com'),
+)
+
+MANAGERS = ADMINS
+
+DB_FILE = join(dirname(__file__), '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 = 'America/Chicago'
+
+# 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 = ''
+
+# 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 = '/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.
+)
+
+# 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 = 'tester.urls'
+
+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.
+ expanduser("~/stuff/django/tester/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',
+ '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,
+ 'handlers': {
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'class': 'django.utils.log.AdminEmailHandler'
+ }
+ },
+ 'loggers': {
+ 'django.request': {
+ 'handlers': ['mail_admins'],
+ 'level': 'ERROR',
+ 'propagate': True,
+ },
+ }
+}
+
+AUTH_PROFILE_MODULE = 'exam.Profile' \ No newline at end of file
diff --git a/templates/exam/complete.html b/templates/exam/complete.html
new file mode 100644
index 0000000..e42704f
--- /dev/null
+++ b/templates/exam/complete.html
@@ -0,0 +1,3 @@
+<p>Quiz is complete. Thank you. </p>
+<br />
+<p>You may now close the browser.</p>
diff --git a/templates/exam/index.html b/templates/exam/index.html
new file mode 100644
index 0000000..5470cf5
--- /dev/null
+++ b/templates/exam/index.html
@@ -0,0 +1,12 @@
+<p> Welcome to the Examination. </p>
+<br></br>
+
+{% if question_list %}
+ <ul>
+ {% for question in question_list %}
+ <li> <a href="/exam/{{ question.id }}/">{{ question.summary }} </a> </li>
+ {% endfor %}
+ </ul>
+{% else %}
+ <p> Lucky you, no questions available.</p>
+{% endif %}
diff --git a/templates/exam/question.html b/templates/exam/question.html
new file mode 100644
index 0000000..05e80a8
--- /dev/null
+++ b/templates/exam/question.html
@@ -0,0 +1,20 @@
+<h2> {{ question.summary }} </h2>
+
+<p>{{ question.question }} </p>
+
+{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
+
+<form action="/exam/{{ question.id }}/check/" method="post">
+{% csrf_token %}
+
+<textarea rows="20" cols="100" name="answer">
+# Enter your answer here.
+</textarea>
+
+<br/>
+
+<input type="submit" name="check" value="Check Answer" />
+<input type="submit" name="skip" value="Skip question" />
+</form>
+
+<p> You have {{quiz.questions_left}} question(s) left. </p> \ No newline at end of file
diff --git a/templates/exam/register.html b/templates/exam/register.html
new file mode 100644
index 0000000..414da72
--- /dev/null
+++ b/templates/exam/register.html
@@ -0,0 +1,7 @@
+Please provide the following details before you start the test.
+<form action="" method="post">
+{% csrf_token %}
+
+{{ form.as_p }}
+<input type="submit" value="submit">
+</form>
diff --git a/urls.py b/urls.py
new file mode 100644
index 0000000..f55309a
--- /dev/null
+++ b/urls.py
@@ -0,0 +1,14 @@
+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()
+
+urlpatterns = patterns('',
+ url(r'^exam/', 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(r'^admin/', include(admin.site.urls)),
+)