summaryrefslogtreecommitdiff
path: root/testapp/exam
diff options
context:
space:
mode:
Diffstat (limited to 'testapp/exam')
-rw-r--r--testapp/exam/__init__.py0
-rw-r--r--testapp/exam/admin.py5
-rw-r--r--testapp/exam/forms.py95
-rw-r--r--testapp/exam/management/__init__.py0
-rw-r--r--testapp/exam/management/commands/__init__.py0
-rw-r--r--testapp/exam/management/commands/dump_user_data.py98
-rw-r--r--testapp/exam/management/commands/load_exam.py55
-rw-r--r--testapp/exam/management/commands/load_questions_xml.py73
-rw-r--r--testapp/exam/management/commands/results2csv.py69
-rw-r--r--testapp/exam/migrations/0001_initial.py193
-rw-r--r--testapp/exam/migrations/__init__.py0
-rw-r--r--testapp/exam/models.py221
-rw-r--r--testapp/exam/tests.py16
-rw-r--r--testapp/exam/urls.py16
-rw-r--r--testapp/exam/views.py351
-rw-r--r--testapp/exam/xmlrpc_clients.py78
16 files changed, 1270 insertions, 0 deletions
diff --git a/testapp/exam/__init__.py b/testapp/exam/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testapp/exam/__init__.py
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
--- /dev/null
+++ b/testapp/exam/management/__init__.py
diff --git a/testapp/exam/management/commands/__init__.py b/testapp/exam/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testapp/exam/management/commands/__init__.py
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 = '<username1> ... <usernamen>'
+ 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 = '<q_file1.py q_file2.py>'
+ 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 = '<q_file1.xml q_file2.xml>'
+ 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 = '<results.csv>'
+ 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
--- /dev/null
+++ b/testapp/exam/migrations/__init__.py
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<quiz_id>\d+)/$', 'monitor'),
+ url(r'^user_data/(?P<username>[a-zA-Z0-9_.]+)/$', 'user_data'),
+ url(r'^grade_user/(?P<username>[a-zA-Z0-9_.]+)/$', 'grade_user'),
+ url(r'^(?P<q_id>\d+)/$', 'question'),
+ url(r'^(?P<q_id>\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()
+