summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPrabhu Ramachandran2011-11-23 14:58:16 +0530
committerPrabhu Ramachandran2011-11-23 14:58:16 +0530
commit30f56443790841901f15b5ab435f97fba1c81d85 (patch)
tree006abd7932ca468661f5481e998c6a79f3058ecd
parentba2097a382b581dacced5cb9bd70087396a054f0 (diff)
downloadonline_test-30f56443790841901f15b5ab435f97fba1c81d85.tar.gz
online_test-30f56443790841901f15b5ab435f97fba1c81d85.tar.bz2
online_test-30f56443790841901f15b5ab435f97fba1c81d85.zip
ENH: Cleanup and adding error/comments for answers
Adding error and marks field to each answer. Adding a new comment field to the question paper and also a profile field for convenience. Changing the views, templates and dump scripts to use the models rather than Python code. This cleans things up a lot more. The user data logged and printed is also way more comprehensive, paving the way for easy online grading as well in the next phase of changes.
-rw-r--r--exam/management/commands/dump_user_data.py63
-rw-r--r--exam/management/commands/results2csv.py27
-rw-r--r--exam/models.py45
-rw-r--r--exam/views.py155
-rw-r--r--templates/exam/monitor.html34
-rw-r--r--templates/exam/user_data.html59
6 files changed, 185 insertions, 198 deletions
diff --git a/exam/management/commands/dump_user_data.py b/exam/management/commands/dump_user_data.py
index f14e144..f081565 100644
--- a/exam/management/commands/dump_user_data.py
+++ b/exam/management/commands/dump_user_data.py
@@ -8,34 +8,45 @@ from exam.models import User
data_template = Template('''\
===============================================================================
-Data for {{ user_data.name.title }} ({{ user_data.username }})
+Data for {{ data.user.get_full_name.title }} ({{ data.user.username }})
-Name: {{ user_data.name.title }}
-Username: {{ user_data.username }}
-Roll number: {{ user_data.rollno }}
-Email: {{ user_data.email }}
-Position: {{ user_data.position }}
-Department: {{ user_data.department }}
-Institute: {{ user_data.institute }}
-Date joined: {{ user_data.date_joined }}
-Last login: {{ user_data.last_login }}
-{% for paper in user_data.papers %}
-Paper: {{ paper.name }}
------------------------------------------
-Total marks: {{ paper.total }}
-Questions correctly answered: {{ paper.answered }}
-Total attempts at questions: {{ paper.attempts }}
-Start time: {{ paper.start_time }}
-User IP address: {{ paper.user_ip }}
-{% if paper.answers %}
+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, answer in paper.answers.items %}
-Question: {{ question }}
-{{ answer|safe }}
-{% endfor %} \
-{% endif %} {# if paper.answers #} \
-{% endfor %} {# for paper in user_data.papers #}
+{% for question, answers in paper.get_question_answers.items %}
+Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})
+{% for answer in answers %}\
+###############################################################################
+{{ answer.answer|safe }}
+# Autocheck: {{ answer.error|safe }}
+# Marks: {{ answer.marks }}
+{% endfor %}{# for answer in answers #}\
+{% endfor %}{# for question, answers ... #}\
+
+Teacher comments
+-----------------
+{{ paper.comments|default:"None" }}
+{% endif %}{# if paper.answers.count #}\
+{% endfor %}{# for paper in data.papers #}
''')
@@ -60,7 +71,7 @@ def dump_user_data(unames, stdout):
for user in users:
data = get_user_data(user.username)
- context = Context({'user_data': data})
+ context = Context({'data': data})
result = data_template.render(context)
stdout.write(result)
diff --git a/exam/management/commands/results2csv.py b/exam/management/commands/results2csv.py
index f1da1a8..2993745 100644
--- a/exam/management/commands/results2csv.py
+++ b/exam/management/commands/results2csv.py
@@ -7,16 +7,22 @@ from django.core.management.base import BaseCommand
from django.template import Template, Context
# Local imports.
-from exam.views import get_quiz_data
-from exam.models import Quiz
+from exam.models import Quiz, QuestionPaper
result_template = Template('''\
-"name","username","rollno","email","answered","total","attempts","position","department","institute"
-{% for paper in paper_list %}\
-"{{ paper.name }}","{{ paper.username }}","{{ paper.rollno }}",\
-"{{ paper.email }}","{{ paper.answered }}",{{ paper.total }},\
-{{ paper.attempts }},"{{ paper.position }}",\
-"{{ paper.department }}","{{ paper.institute }}"
+"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 %}\
''')
@@ -39,12 +45,13 @@ def results2csv(filename, stdout):
else:
quiz = qs[0]
- paper_list = get_quiz_data(quiz.id)
+ 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({'paper_list': paper_list})
+ context = Context({'papers': papers})
f.write(result_template.render(context))
f.close()
diff --git a/exam/models.py b/exam/models.py
index fb06576..d433c7c 100644
--- a/exam/models.py
+++ b/exam/models.py
@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
################################################################################
class Profile(models.Model):
"""Profile for a user to store roll number and other details."""
- user = models.ForeignKey(User)
+ user = models.OneToOneField(User)
roll_number = models.CharField(max_length=20)
institute = models.CharField(max_length=128)
department = models.CharField(max_length=64)
@@ -44,8 +44,15 @@ class Answer(models.Model):
# The question for which we are an answer.
question = models.ForeignKey(Question)
- # The last answer submitted by the user.
+ # 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)
@@ -86,6 +93,10 @@ class QuestionPaper(models.Model):
"""
# 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)
@@ -108,6 +119,9 @@ class QuestionPaper(models.Model):
# 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."""
@@ -125,7 +139,7 @@ class QuestionPaper(models.Model):
else:
return qs.count('|') + 1
- def answered_question(self, question_id):
+ def completed_question(self, question_id):
"""Removes the question from the list of questions and returns
the next."""
qa = self.questions_answered
@@ -141,7 +155,7 @@ class QuestionPaper(models.Model):
return ''
else:
return qs[0]
-
+
def skip(self):
"""Skip the current question and return the next available question."""
qs = self.questions.split('|')
@@ -166,6 +180,29 @@ class QuestionPaper(models.Model):
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
diff --git a/exam/views.py b/exam/views.py
index b880c4d..bafd0be 100644
--- a/exam/views.py
+++ b/exam/views.py
@@ -121,7 +121,12 @@ def start(request):
except QuestionPaper.DoesNotExist:
ip = request.META['REMOTE_ADDR']
key = gen_key(10)
- new_paper = QuestionPaper(user=user, user_ip=ip, key=key, quiz=quiz)
+ try:
+ profile = user.get_profile()
+ except Profile.DoesNotExist:
+ profile = None
+ new_paper = QuestionPaper(user=user, user_ip=ip, key=key,
+ quiz=quiz, profile=profile)
new_paper.start_time = datetime.datetime.now()
# Make user directory.
@@ -199,11 +204,14 @@ def check(request, q_id):
# running as nobody.
user_dir = get_user_dir(user)
success, err_msg = python_server.run_code(answer, question.test, user_dir)
-
+ new_answer.error = err_msg
+
if success:
- # Note the success and save it.
+ # Note the success and save it along with the marks.
new_answer.correct = success
- new_answer.save()
+ new_answer.marks = question.points
+
+ new_answer.save()
ci = RequestContext(request)
if not success:
@@ -221,7 +229,7 @@ def check(request, q_id):
return my_render_to_response('exam/question.html', context,
context_instance=ci)
else:
- next_q = paper.answered_question(question.id)
+ next_q = paper.completed_question(question.id)
return show_question(request, next_q)
def quit(request):
@@ -241,151 +249,64 @@ def complete(request, reason=None):
return my_render_to_response('exam/complete.html', context)
else:
return my_redirect('/exam/')
-
-def get_quiz_data(quiz_id):
- """Convenience function to get all quiz results. This is used by other
- functions. Returns a list containing one dictionary for each question paper
- of the quiz. The dictionary contains the necessary data.
- """
- try:
- quiz = Quiz.objects.get(id=quiz_id)
- except Quiz.DoesNotExist:
- q_papers = []
- else:
- q_papers = QuestionPaper.objects.filter(quiz=quiz)
- questions = Question.objects.all()
- # Mapping from question id to points
- marks = dict( ( (q.id, q.points) for q in questions) )
-
- paper_list = []
- for q_paper in q_papers:
- paper = {}
- user = q_paper.user
- try:
- profile = Profile.objects.get(user=user)
- except Profile.DoesNotExist:
- # Admin user may have a paper by accident but no profile.
- continue
- paper['name'] = user.get_full_name()
- paper['username'] = user.username
- paper['rollno'] = str(profile.roll_number)
- paper['institute'] = str(profile.institute)
- paper['email'] = user.email
- qa = q_paper.questions_answered.split('|')
- answered = ', '.join(sorted(qa))
- paper['answered'] = answered if answered else 'None'
- paper['attempts'] = q_paper.answers.count()
- total = sum( [marks[int(id)] for id in qa if id] )
- paper['total'] = total
- paper_list.append(paper)
-
- return paper_list
-
+
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()
- quiz_data = {}
- for quiz in quizzes:
- quiz_data[quiz.id] = quiz.description
- context = {'paper_list': [],
- 'quiz_name': '',
- 'quiz_data':quiz_data}
+ 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:
- quiz = None
-
- quiz_data = {}
- paper_list = get_quiz_data(quiz_id)
-
- if quiz is None:
- quiz_name = 'No active quiz'
- elif len(quiz.description) > 0:
- quiz_name = quiz.description
+ papers = []
else:
- quiz_name = 'Quiz'
-
- paper_list.sort(cmp=lambda x, y: cmp(x['total'], y['total']),
- reverse=True)
+ papers = QuestionPaper.objects.filter(quiz=quiz,
+ user__profile__isnull=False)
- context = {'paper_list': paper_list, 'quiz_name': quiz_name}
+ sorted(papers,
+ cmp=lambda x, y: cmp(x.get_total_marks(), y.get_total_marks()),
+ reverse=True)
+
+ 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.
"""
- try:
- user = User.objects.get(username=username)
- except User.DoesNotExist:
- q_papers = []
- else:
- q_papers = QuestionPaper.objects.filter(user=user)
- questions = Question.objects.all()
- # Mapping from question id to points
- marks = dict( ( (q.id, q.points) for q in questions) )
+ user = User.objects.get(username=username)
+ papers = QuestionPaper.objects.filter(user=user)
data = {}
try:
- profile = Profile.objects.get(user=user)
+ profile = user.get_profile()
except Profile.DoesNotExist:
# Admin user may have a paper by accident but no profile.
profile = None
- data['username'] = user.username
- data['email'] = user.email
- data['rollno'] = profile.roll_number if profile else ''
- data['name'] = user.get_full_name()
- data['institute'] = profile.institute if profile else ''
- data['department'] = profile.department if profile else ''
- data['position'] = profile.position if profile else ''
- data['name'] = user.get_full_name()
- data['date_joined'] = str(user.date_joined)
- data['last_login'] = str(user.last_login)
- papers = []
- for q_paper in q_papers:
- paper = {}
- paper['name'] = q_paper.quiz.description
- qa = q_paper.questions_answered.split('|')
- answered = ', '.join(sorted(qa))
- paper['answered'] = answered if answered else 'None'
- paper['attempts'] = q_paper.answers.count()
- total = sum( [marks[int(id)] for id in qa if id] )
- paper['total'] = total
- paper['user_ip'] = q_paper.user_ip
- paper['start_time'] = str(q_paper.start_time)
- answers = {}
- for answer in q_paper.answers.all():
- question = answer.question
- qs = '%d. %s'%(question.id, question.summary)
- code = '#'*80 + '\n' + str(answer.answer) + '\n'
- if qs in answers:
- answers[qs] += code
- else:
- answers[qs] = code
- paper['answers'] = answers
- papers.append(paper)
- data['papers'] = papers
+ 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 = {'user_data': data}
+
+ context = {'data': data}
return my_render_to_response('exam/user_data.html', context,
- context_instance=RequestContext(request))
-
+ context_instance=RequestContext(request))
+
diff --git a/templates/exam/monitor.html b/templates/exam/monitor.html
index 1ce6c69..8f34a7f 100644
--- a/templates/exam/monitor.html
+++ b/templates/exam/monitor.html
@@ -6,7 +6,7 @@
{% block content %}
-{% if not quiz_data and not paper_list %}
+{% if not quizzes and not papers %}
<h1> Quiz results </h1>
<p> No quizzes available. </p>
@@ -15,23 +15,23 @@
{# ############################################################### #}
{# This is rendered when we are just viewing exam/monitor #}
-{% if quiz_data %}
+{% if quizzes %}
<h1> Available quizzes </h1>
<ul>
-{% for quiz_id, quiz_name in quiz_data.items %}
-<li><a href="{{URL_ROOT}}/exam/monitor/{{quiz_id}}/">{{ quiz_name }}</a></li>
+{% for quiz in quizzes %}
+<li><a href="{{URL_ROOT}}/exam/monitor/{{quiz.id}}/">{{ quiz.description }}</a></li>
{% endfor %}
</ul>
{% endif %}
{# ############################################################### #}
{# This is rendered when we are just viewing exam/monitor/quiz_num #}
-{% if paper_list %}
-<h1> {{ quiz_name }} results </h1>
+{% if papers %}
+<h1> {{ quiz.description }} results </h1>
{# <p> Quiz: {{ quiz_name }}</p> #}
-<p>Number of papers: {{ paper_list|length }} </p>
+<p>Number of papers: {{ papers|length }} </p>
<table border="1" cellpadding="3">
<tr>
@@ -43,19 +43,21 @@
<th> Total marks </th>
<th> Attempts </th>
</tr>
- {% for paper in paper_list %}
+ {% for paper in papers %}
<tr>
- <td> <a href="{{URL_ROOT}}/exam/user_data/{{paper.username}}">{{ paper.name.title }}</a> </td>
- <td> <a href="{{URL_ROOT}}/exam/user_data/{{paper.username}}">{{ paper.username }}</a> </td>
- <td> {{ paper.rollno }} </td>
- <td> {{ paper.institute }} </td>
- <td> {{ paper.answered }} </td>
- <td> {{ paper.total }} </td>
- <td> {{ paper.attempts }} </td>
+ <td> <a href="{{URL_ROOT}}/exam/user_data/{{paper.user.username}}">
+ {{ paper.user.get_full_name.title }}</a> </td>
+ <td> <a href="{{URL_ROOT}}/exam/user_data/{{paper.user.username}}">
+ {{ paper.user.username }}</a> </td>
+ <td> {{ paper.profile.roll_number }} </td>
+ <td> {{ paper.profile.institute }} </td>
+ <td> {{ paper.get_answered_str }} </td>
+ <td> {{ paper.get_total_marks }} </td>
+ <td> {{ paper.answers.count }} </td>
</tr>
{% endfor %}
</table>
-{% else %} {% if quiz_name %}
+{% else %} {% if quiz %}
<p> No answer papers so far. </p>
{% endif %}
{% endif %}
diff --git a/templates/exam/user_data.html b/templates/exam/user_data.html
index c2e8014..7563e0e 100644
--- a/templates/exam/user_data.html
+++ b/templates/exam/user_data.html
@@ -1,49 +1,58 @@
{% extends "base.html" %}
-{% block title %} Data for user {{ user_data.name.title }} {% endblock title %}
+{% block title %} Data for user {{ data.user.get_full_name.title }} {% endblock title %}
{% block content %}
-<h1> Data for user {{ user_data.name.title }} </h1>
+<h1> Data for user {{ data.user.get_full_name.title }} </h1>
<p>
-Name: {{ user_data.name.title }} <br/>
-Username: {{ user_data.username }} <br/>
-Roll number: {{ user_data.rollno }} <br/>
-Email: {{ user_data.email }} <br/>
-Position: {{ user_data.position }} <br/>
-Department: {{ user_data.department }} <br/>
-Institute: {{ user_data.institute }} <br/>
-Date joined: {{ user_data.date_joined }} <br/>
-Last login: {{ user_data.last_login }}
+Name: {{ data.user.get_full_name.title }} <br/>
+Username: {{ data.user.username }} <br/>
+{% if data.profile %}
+Roll number: {{ data.profile.roll_number }} <br/>
+Position: {{ data.profile.position }} <br/>
+Department: {{ data.profile.department }} <br/>
+Institute: {{ data.profile.institute }} <br/>
+{% endif %}
+Email: {{ data.user.email }} <br/>
+Date joined: {{ data.user.date_joined }} <br/>
+Last login: {{ data.user.last_login }}
</p>
-{% if user_data.papers %}
+{% if data.papers %}
-{% for paper in user_data.papers %}
+{% for paper in data.papers %}
-<h2> Quiz: {{ paper.name }} </h2>
+<h2> Quiz: {{ paper.quiz.description }} </h2>
<p>
-Answered questions: {{ paper.answered }} <br/>
-Total attempts at questions: {{ paper.attempts }} <br/>
-Total marks: {{ paper.total }} <br/>
+Questions correctly answered: {{ paper.get_answered_str }} <br/>
+Total attempts at questions: {{ paper.answers.count }} <br/>
+Marks obtained: {{ paper.get_total_marks }} <br/>
Start time: {{ paper.start_time }} <br/>
User IP address: {{ paper.user_ip }}
</p>
-{% if paper.answers %}
+{% if paper.answers.count %}
<h3> Answers </h3>
-{% for question, answer in paper.answers.items %}
-<p><strong> Question: {{ question }} </strong> </p>
+{% for question, answers in paper.get_question_answers.items %}
+<p><strong> Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})</strong> </p>
+{% for answer in answers %}
<pre>
-{{ answer|safe }}
+################################################################################
+{{ answer.answer|safe }}
+# Autocheck: {{ answer.error }}
+# Marks: {{ answer.marks }}
</pre>
-{% endfor %}
-{% endif %} {# if paper.answers #}
+{% endfor %} {# for answer in answers #}
+{% endfor %} {# for question, answers ... #}
+<h3>Teacher comments: </h3>
+{{ paper.comments|default:"None" }}
+{% endif %} {# if paper.answers.count #}
-{% endfor %} {# for paper in user_data.papers #}
+{% endfor %} {# for paper in data.papers #}
-{% endif %} {# if user_data.papers #}
+{% endif %} {# if data.papers #}
{% endblock content %}