From 30f56443790841901f15b5ab435f97fba1c81d85 Mon Sep 17 00:00:00 2001 From: Prabhu Ramachandran Date: Wed, 23 Nov 2011 14:58:16 +0530 Subject: 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. --- exam/management/commands/dump_user_data.py | 63 +++++++----- exam/management/commands/results2csv.py | 27 +++-- exam/models.py | 45 ++++++++- exam/views.py | 155 +++++++---------------------- templates/exam/monitor.html | 34 ++++--- templates/exam/user_data.html | 59 ++++++----- 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 %}
No quizzes available.
@@ -15,23 +15,23 @@ {# ############################################################### #} {# This is rendered when we are just viewing exam/monitor #} -{% if quiz_data %} +{% if quizzes %}Quiz: {{ quiz_name }}
#} -Number of papers: {{ paper_list|length }}
+Number of papers: {{ papers|length }}
Total marks | Attempts | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{{ paper.name.title }} | -{{ paper.username }} | -{{ paper.rollno }} | -{{ paper.institute }} | -{{ paper.answered }} | -{{ paper.total }} | -{{ paper.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 }} |
No answer papers so far.
{% 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 %} -
-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 }}
+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 }}
-Answered questions: {{ paper.answered }}
-Total attempts at questions: {{ paper.attempts }}
-Total marks: {{ paper.total }}
+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 }}
Question: {{ question }}
+{% for question, answers in paper.get_question_answers.items %} +Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})
+{% for answer in answers %}-{{ answer|safe }} +################################################################################ +{{ answer.answer|safe }} +# Autocheck: {{ answer.error }} +# Marks: {{ answer.marks }}-{% endfor %} -{% endif %} {# if paper.answers #} +{% endfor %} {# for answer in answers #} +{% endfor %} {# for question, answers ... #} +