diff options
author | Prabhu Ramachandran | 2011-11-23 14:58:16 +0530 |
---|---|---|
committer | Prabhu Ramachandran | 2011-11-23 14:58:16 +0530 |
commit | 30f56443790841901f15b5ab435f97fba1c81d85 (patch) | |
tree | 006abd7932ca468661f5481e998c6a79f3058ecd /exam | |
parent | ba2097a382b581dacced5cb9bd70087396a054f0 (diff) | |
download | online_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.
Diffstat (limited to 'exam')
-rw-r--r-- | exam/management/commands/dump_user_data.py | 63 | ||||
-rw-r--r-- | exam/management/commands/results2csv.py | 27 | ||||
-rw-r--r-- | exam/models.py | 45 | ||||
-rw-r--r-- | exam/views.py | 155 |
4 files changed, 133 insertions, 157 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)) + |