--- /dev/null
+Django quiz app
+===============
+
+This is a configurable quiz app for Django.
+
+I use it to run a few medical revision websites. Here is an [example website](http://www.revisemrcp.com/)
+
+My websites have used twitter bootstrap for the front end and I have tried to strip out anything from
+the template files that are dependant on bootstrap.
+
+This is a major work in progress.
+
+Current features
+----------------
+Features of each quiz:
+* Question order randomisation
+* Storing of quiz results under each user
+* Previous quiz scores can be viewed on category page
+* Correct answers can be shown after each question or all at once at the end
+* Logged in users can return to an incomplete quiz to finish it
+
+Multiple choice questions
+* Questions can be given a category
+* Success rate for each category can be monitored on a progress page
+* Explanation for each question result can be given
+
+This is my first open source project so please forgive any problems and/or dreadful code!
\ No newline at end of file
--- /dev/null
+from django.db import models
+from quiz.models import Quiz, Category
+
+"""
+Multiple choice style question for quiz
+
+"""
+
+class Question(models.Model):
+
+ quiz = models.ManyToManyField(Quiz, blank=True, )
+
+ category = models.ForeignKey(Category, blank=True, null=True, )
+
+ content = models.CharField(max_length=1000,
+ blank=False,
+ help_text="Enter the question text that you want displayed",
+ verbose_name='Question',
+ )
+
+ explanation = models.TextField(max_length=2000,
+ blank=True,
+ help_text="Explanation to be shown after the question has been answered.",
+ verbose_name='Explanation',
+ )
+
+
+ class Meta:
+ verbose_name = "Question"
+ verbose_name_plural = "Questions"
+ ordering = ['category']
+
+
+ def __unicode__(self):
+ return self.content
+
+
+class Answer(models.Model):
+ question = models.ForeignKey(Question)
+
+ content = models.CharField(max_length=1000,
+ blank=False,
+ help_text="Enter the answer text that you want displayed",
+ )
+
+ correct = models.BooleanField(blank=False,
+ default=False,
+ help_text="Is this a correct answer?"
+ )
+
+ def __unicode__(self):
+ return self.content
\ No newline at end of file
--- /dev/null
+"""
+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)
--- /dev/null
+# Create your views here.
--- /dev/null
+from django import forms
+from django.contrib import admin
+from django.contrib.admin.widgets import FilteredSelectMultiple
+from quiz.models import Quiz, Category, Progress
+from multichoice.models import Question, Answer
+
+class QuestionInline(admin.TabularInline):
+ model = Question.quiz.through
+ filter_horizontal = ('content',)
+
+
+class AnswerInline(admin.TabularInline):
+ model = Answer
+
+"""
+below is from
+http://stackoverflow.com/questions/11657682/django-admin-interface-using-horizontal-filter-with-inline-manytomany-field
+"""
+
+class QuizAdminForm(forms.ModelForm):
+ class Meta:
+ model = Quiz
+
+ questions = forms.ModelMultipleChoiceField(
+ queryset=Question.objects.all(),
+ required=False,
+ widget=FilteredSelectMultiple(verbose_name=('Questions'),
+ is_stacked=False
+ )
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(QuizAdminForm, self).__init__(*args, **kwargs)
+ if self.instance.pk:
+ self.fields['questions'].initial = self.instance.question_set.all()
+
+ def save(self, commit=True):
+ quiz = super(QuizAdminForm, self).save(commit=False)
+ if commit:
+ quiz.save()
+ if quiz.pk:
+ quiz.question_set = self.cleaned_data['questions']
+ self.save_m2m()
+ return quiz
+
+class QuizAdmin(admin.ModelAdmin):
+ form = QuizAdminForm
+
+ list_display = ('title', 'category', )
+ list_filter = ('category',)
+ search_fields = ('description', 'category', )
+
+
+
+class CategoryAdmin(admin.ModelAdmin):
+ search_fields = ('category', )
+
+class QuestionAdmin(admin.ModelAdmin):
+ list_display = ('content', 'category', )
+ list_filter = ('category',)
+ fields = ('content', 'category', 'quiz', 'explanation' )
+
+ search_fields = ('content', )
+ filter_horizontal = ('quiz',)
+
+
+ inlines = [AnswerInline]
+
+
+class ProgressAdmin(admin.ModelAdmin):
+ """
+ to do:
+ create a user section
+ """
+ search_fields = ('user', 'score', )
+
+admin.site.register(Quiz, QuizAdmin)
+admin.site.register(Category, CategoryAdmin)
+admin.site.register(Question, QuestionAdmin)
+admin.site.register(Progress, ProgressAdmin)
\ No newline at end of file
--- /dev/null
+from django import forms
+from django.forms.models import inlineformset_factory
+
+from multichoice.models import Question, Answer
+
--- /dev/null
+from django.db import models
+from django.utils.encoding import smart_str
+from django.conf import settings
+from django.contrib.auth.models import User
+
+
+
+"""
+Quiz is a container that can be filled with various different question types
+or other content
+"""
+
+
+class CategoryManager(models.Manager):
+ """
+ custom manager for Progress class
+ """
+ def new_category(self, category):
+ """
+ add a new category
+ """
+ new_category = self.create(category=category)
+ new_category.save()
+
+class Category(models.Model):
+
+ category = models.CharField(max_length=250,
+ blank=True,
+ choices=CATEGORY_CHOICES,
+ unique=True,
+ null=True,
+ )
+
+
+ class Meta:
+ verbose_name = "Category"
+ verbose_name_plural = "Categories"
+
+ def __unicode__(self):
+ return self.category
+
+
+class Quiz(models.Model):
+
+ title = models.CharField(max_length=60)
+
+ description = models.TextField(blank=True)
+
+ url = models.CharField(max_length=60,
+ blank=False,
+ help_text="an SEO friendly url",
+ verbose_name='SEO friendly url',
+ )
+
+ category = models.ForeignKey(Category, null=True, blank=True, )
+
+ random_order = models.BooleanField(blank=False,
+ default=False,
+ help_text="Display the questions in a random order or as they are set?",
+ )
+
+ answers_at_end = models.BooleanField(blank=False,
+ default=False,
+ help_text="Correct answer is NOT shown after question. Answers displayed at end",
+ )
+
+ exam_paper = models.BooleanField(blank=False,
+ default=False,
+ help_text="If yes, the result of each attempt by a user will be stored",
+ )
+
+
+ def save(self, force_insert=False, force_update=False): # automatically converts url to lowercase
+ self.url = self.url.lower()
+ super(Quiz, self).save(force_insert, force_update)
+
+
+ class Meta:
+ verbose_name = "Quiz"
+ verbose_name_plural = "Quizzes"
+
+
+ def __unicode__(self):
+ return self.title
+
+
+class ProgressManager(models.Manager):
+ """
+ custom manager for Progress class
+ """
+ def new_progress(self, user):
+ """
+ method to call when a user is accessing the progress section for the first time
+ """
+ new_progress = self.create(user=user, score='')
+ new_progress.save()
+ return new_progress
+
+class Progress(models.Model):
+ """
+ Stores the score for each category, max possible and previous exam paper scores
+
+ data stored in csv using the format [category, score, possible, category, score, possible, ...]
+
+ to do:
+ combine the check and update functions into one
+ """
+
+ user = models.OneToOneField('auth.User') # one user per progress class
+
+ score = models.TextField() # the god awful csv. guido forgive me. Always end this with a comma
+
+ objects = ProgressManager()
+
+ def list_all_cat_scores(self):
+ """
+ Returns a dict in which the key is the category name and the item is a list of three integers.
+ The first is the number of questions correct, the second is the possible best score,
+ the third is the percentage correct
+ """
+ import re # uh oh
+
+ categories = Category.objects.all() # all the categories possible
+ output = {}
+
+ for cat in categories: # for each of the categories
+ my_regex = re.escape(cat.category) + r",(\d+),(\d+)," # group 1 is score, group 2 is possible
+ match = re.search(my_regex, self.score, re.IGNORECASE)
+
+ if match:
+ score = int(match.group(1))
+ possible = int(match.group(2))
+ percent = int(round((float(score) / float(possible)) * 100))
+ score_list = [score, possible, percent]
+ output[cat.category] = score_list
+
+
+ else: # Is possible to remove/comment this section out
+ temp = self.score
+ temp = temp + cat.category + ",0,0," # always end with a comma
+ self.score = temp
+ output[cat.category] = [0, 0]
+ self.save() # add this new output to disk
+
+ return output
+
+
+ def check_cat_score(self, category_queried):
+ """
+ pass in a category, get the users score and possible score as x,y respectively
+
+ note: score returned as integers
+ """
+
+ category_test = Category.objects.filter(category=category_queried).exists()
+
+ if category_test == False:
+ return "error", "category does not exist" # to do: update
+
+ import re # :'( always a bad start
+ my_regex = re.escape(category_queried) + r",(\d+),(\d+)," # group 1 is score, group 2 is possible
+
+ match = re.search(my_regex, self.score, re.IGNORECASE)
+
+ if match:
+ score = int(match.group(1))
+ possible = int(match.group(2))
+ return score, possible
+
+ else: # if not found, and since category exists, add category to the csv with 0 points
+ """
+ # removed to lower disk writes
+ temp = self.score
+ temp = temp + category_queried + ",0,0," # always end with a comma
+ self.score = temp
+ self.save()
+ """
+ return 0, 0
+
+ def update_score(self, category_queried, score_to_add, possible_to_add):
+ """
+ pass in category, amount to increase score and max possible increase if all were correct
+
+ does not return anything
+
+ data stored in csv using the format [category, score, possible, category, score, possible, ...]
+ """
+
+ category_test = Category.objects.filter(category=category_queried).exists()
+
+ if category_test == False:
+ return "error", "category does not exist" # to do: update
+
+ import re # :'( always a bad start
+ my_regex = re.escape(str(category_queried)) + r",(\d+),(\d+)," # group 1 is score, group 2 is possible
+
+ match = re.search(my_regex, self.score, re.IGNORECASE)
+
+ if match:
+ current_score = int(match.group(1))
+ current_possible = int(match.group(2))
+
+ updated_current_score = current_score + score_to_add
+ updated_current_possible = current_possible + possible_to_add
+
+ new_score = str(category_queried) + "," + str(updated_current_score) + "," + str(updated_current_possible) + ","
+
+ temp = self.score
+ found_instance = match.group()
+ temp = temp.replace(found_instance, new_score) # swap the old score for the new one
+
+ self.score = temp
+ self.save()
+
+
+ else:
+ """
+ if not present but a verified category, add with the points passed in
+ """
+ temp = self.score
+ temp = temp + str(category_queried) + "," + str(score_to_add) + "," + str(possible_to_add) + ","
+ self.score = temp
+ self.save()
+
+
+ def show_exams(self,):
+ """
+ finds the previous exams marked as 'exam papers'
+
+ returns a queryset of the exams
+ """
+
+ exams = Sitting.objects.filter(user=self.user).filter(complete=True) # list of exam objects from user that are complete
+
+ return exams
+
+
+class SittingManager(models.Manager):
+ """
+ custom manager for Sitting class
+ """
+ def new_sitting(self, user, quiz):
+ """
+ method to call at the start of each new attempt at a quiz
+ """
+ if quiz.random_order == True:
+ question_set = quiz.question_set.all().order_by('?')
+ else:
+ question_set = quiz.question_set.all()
+
+ questions = ""
+ for question in question_set:
+ questions = questions + str(question.id) + "," # string of IDs seperated by commas
+
+ new_sitting = self.create(user=user,
+ quiz=quiz,
+ question_list = questions,
+ incorrect_questions = "",
+ current_score="0",
+ complete=False,
+ )
+ new_sitting.save()
+ return new_sitting
+
+
+class Sitting(models.Model):
+ """
+ Used to store the progress of logged in users sitting an exam. Replaces the session system used by anon users.
+
+ user is the logged in user. Anon users use sessions to track progress
+ quiz
+ question_list is a list of id's of the unanswered questions. Stored as a textfield to allow >255 chars. CSV
+ incorrect_questions is a list of id's of the questions answered wrongly
+ current_Score is total of answered questions value. Needs to be converted to int when used.
+ complete - True when exam complete. Should only be stored if quiz.exam_paper is trued, or DB will become huge
+ """
+
+ user = models.ForeignKey('auth.User') # one user per exam class
+
+ quiz = models.ForeignKey(Quiz)
+
+ question_list = models.TextField() # another awful csv. Always end with a comma
+
+ incorrect_questions = models.TextField(blank=True) # more awful csv. Always end with a comma
+
+ current_score = models.TextField() # a string of the score ie 19 convert to int for use
+
+ complete = models.BooleanField(default=False,)
+
+ objects = SittingManager()
+
+ def get_next_question(self):
+ """
+ Returns the next question ID (as an integer).
+ Does NOT remove the question from the front of the list.
+ """
+ first_comma = self.question_list.find(',') # finds the index of the first comma in the string
+ if first_comma == -1 or first_comma == 0: # if no question number is found
+ return False
+ qID = self.question_list[:first_comma] # up to but not including the first comma
+
+ return qID
+
+ def remove_first_question(self):
+ """
+ Removes the first question on the list.
+ Does not return a value.
+ """
+ first_comma = self.question_list.find(',') # finds the index of the first comma in the string
+ if first_comma != -1 or first_comma != 0: # if question number IS found
+ temp = self.question_list[first_comma+1:] # saves from the first number after the first comma
+ self.question_list = temp
+ self.save()
+
+ def add_to_score(self, points):
+ """
+ Adds the input (points) to the running total.
+ Does not return anything
+ """
+ present_score = self.get_current_score()
+ present_score = present_score + points
+ self.current_score = str(present_score)
+
+ def get_current_score(self):
+ """
+ returns the current score as an integer
+ """
+ return int(self.current_score)
+
+ def mark_quiz_complete(self):
+ """
+ Changes the quiz to complete.
+ Does not return anything
+ """
+ self.complete = True
+ self.save()
+
+ def add_incorrect_question(self, question):
+ """
+ Adds the uid of an incorrect question to the list of incorrect questions
+ Does not return anything
+ """
+ current = self.incorrect_questions
+ question_id = question.id
+ if current == "":
+ updated = str(question_id) + ","
+ else:
+ updated = current + str(question_id) + ","
+ self.incorrect_questions = updated
+ self.save()
+
+ def get_incorrect_questions(self):
+ """
+ Returns a list of IDs that indicate all the questions that have been answered incorrectly in this sitting
+ """
+ question_list = self.incorrect_questions # string of question IDs as CSV ie 32,19,22,3,75
+ split_questions = question_list.split(',') # list of numbers [32,19,22,3,75]
+ return split_questions
\ No newline at end of file
--- /dev/null
+from django import template
+from multichoice.models import Question, Answer
+
+register = template.Library()
+
+@register.inclusion_tag('answers_for_question.html', takes_context=True)
+def answers_for_question(context, question, quiz):
+ """
+ Displays the possible answers to a question
+ """
+ answers = Answer.objects.filter(question__id=question.id).order_by('?')
+ return {'answers': answers, 'quiz': quiz}
+
+@register.inclusion_tag('correct_answer.html', takes_context=True)
+def correct_answer(context, previous):
+ """
+ processes the correct answer based on the previous question dict
+ """
+ q = previous['previous_question']
+ answers = Answer.objects.filter(question__id=q.id)
+ return {'answers': answers, }
+
+@register.inclusion_tag('correct_answer.html', takes_context=True)
+def correct_answer_for_all(context, question):
+ """
+ processes the correct answer based on a given question object
+ """
+ answers = Answer.objects.filter(question__id=question.id)
+ return {'answers': answers, }
+
+@register.inclusion_tag('correct_answer.html', takes_context=True)
+def correct_answer_for_all_with_users_incorrect(context, question, incorrect_list):
+ """
+ processes the correct answer based on a given question object
+ if the answer is incorrect, informs the user
+ """
+ answers = Answer.objects.filter(question__id=question.id) # if question was answered wrongly, tell the user
+ question_id = str(question.id)
+ if question_id in incorrect_list:
+ user_was_incorrect = True
+ else:
+ user_was_incorrect = False
+ return {'answers': answers, 'user_was_incorrect': user_was_incorrect, }
\ No newline at end of file
--- /dev/null
+"""
+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)
--- /dev/null
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+
+ # progress
+ url(r'^progress/$', 'quiz.views.progress'),
+ url(r'^progress$', 'quiz.views.progress'),
+
+
+ # passes variable 'quiz_name' to quiz_take view
+ url(r'^(?P<quiz_name>[\w-]+)/$', 'quiz.views.quiz_take'), # quiz/
+ url(r'^(?P<quiz_name>[\w-]+)$', 'quiz.views.quiz_take'), # quiz
+
+
+)
--- /dev/null
+from django.http import HttpResponse
+from django.shortcuts import render_to_response
+from django.core.exceptions import ObjectDoesNotExist
+from django.contrib import auth
+from django.core.context_processors import csrf
+from django.http import HttpResponseRedirect
+from django.template import RequestContext
+
+
+
+from quiz.models import Quiz, Category, Progress, Sitting
+from multichoice.models import Question, Answer
+
+
+"""
+
+Views related directly to the quiz
+
+***********************************
+
+
+used by anon only:
+
+ request.session[q_list] is a list of the remaining question IDs in order. "q_list" = quiz_id + "q_list"
+ request.session[quiz_id + "_score"] is current score. Best possible score is number of questions.
+
+used by both user and anon:
+
+ request.session['page_count'] is a counter used for displaying adverts every X number of pages
+
+question.answer_set.all() is all the answers for question
+quiz.question_set.all() is all the questions in a quiz
+
+To do:
+ variable scores per question
+ seperate the login portion so that other django apps are compatible
+ if a user does some questions as anon, then logs in, remove these questions from remaining q list for logged in user
+ allow the page count to be set in admin
+"""
+
+
+def quiz_take(request, quiz_name):
+ """
+ Initial handler for the quiz.
+ 1. Tests if user is logged in.
+ 2. Decides whether this is the start of a new quiz.
+ """
+
+ quiz = Quiz.objects.get(url=quiz_name.lower()) # url refers to the SEO friendly url specified in model.quiz
+
+ if request.user.is_authenticated() == True: # logged in user
+ try:
+ previous_sitting = Sitting.objects.get(user=request.user,
+ quiz=quiz,
+ complete=False,
+ )
+ except Sitting.DoesNotExist:
+ # start new quiz
+ return user_new_quiz_session(request, quiz)
+
+ except Sitting.MultipleObjectsReturned:
+ # if more than one sitting found
+ previous_sitting = Sitting.objects.filter(user=request.user,
+ quiz=quiz,
+ complete=False,
+ )[0] # use the first one
+ return user_load_next_question(request, previous_sitting, quiz)
+
+ else:
+ # use existing quiz
+ return user_load_next_question(request, previous_sitting, quiz)
+
+
+ else: # anon user
+ quiz_id = str(quiz.id)
+ q_list = quiz_id + "_q_list"
+
+ if q_list in request.session: # check if anon user has a recent session for this quiz
+ return load_anon_next_question(request, quiz) # load up previous session
+ else:
+ return new_anon_quiz_session(request, quiz) # new session for anon user
+
+
+def new_anon_quiz_session(request, quiz):
+ """
+ Sets the session variables when starting a quiz for the first time
+
+ to do:
+ include a cron job to clear the expired sessions daily
+ """
+
+ request.session.set_expiry(259200) # set the session to expire after 3 days
+
+ questions = quiz.question_set.all()
+ question_list = []
+ for question in questions:
+ question_list.append(question.id) # question_list is a list of question IDs, which are integers
+
+ if quiz.random_order == True:
+ import random
+ random.shuffle(question_list)
+
+ quiz_id = str(quiz.id)
+
+ score = quiz_id + "_score"
+ request.session[score] = int(0) # session score for anon users
+
+ q_list = quiz_id + "_q_list"
+ request.session[q_list] = question_list # session list of questions
+
+ request.session['page_count'] = int(0) # session page count for adverts
+
+ return load_anon_next_question(request, quiz)
+
+def user_new_quiz_session(request, quiz):
+ """
+ initialise the Sitting class
+ """
+ sitting = Sitting.objects.new_sitting(request.user, quiz)
+
+ request.session['page_count'] = int(0) # session page count for adverts
+
+ return user_load_next_question(request, sitting, quiz)
+
+
+
+def load_anon_next_question(request, quiz):
+ """
+ load up the next question, including the outcome of the previous question
+ """
+ quiz_id = str(quiz.id)
+ q_list = quiz_id + "_q_list"
+ question_list = request.session[q_list]
+ previous = {}
+
+ if 'guess' in request.GET and request.GET['guess']:
+ # if there has been a previous question
+ previous = question_check_anon(request, quiz) # returns a dictionary with previous question details
+
+ question_list = question_list[1:] # move onto next question
+ request.session[q_list] = question_list
+
+ counter = request.session['page_count']
+ request.session['page_count'] = counter + 1 # add 1 to the page counter
+
+
+
+ if not request.session[q_list]:
+ # no questions left!
+ return final_result_anon(request, quiz, previous)
+
+ show_advert = False
+
+ try:
+ if request.session['page_count'] > 0 and request.session['page_count'] % 10 == 0:
+ # advert page every 10 questions
+ counter = request.session['page_count']
+ request.session['page_count'] = counter + 1 # add 1 to the page counter
+ show_advert = True
+
+ except KeyError:
+ request.session['page_count'] = int(0) # since one hasnt been started, make it now
+
+ next_question_id = question_list[0]
+ question = Question.objects.get(id=next_question_id)
+
+
+ return render_to_response('question.html',
+ {'quiz': quiz,
+ 'question': question,
+ 'previous': previous,
+ 'show_advert': show_advert,
+ },
+ context_instance=RequestContext(request))
+
+
+def user_load_next_question(request, sitting, quiz):
+ """
+ load the next question, including outcome of previous question, using the sitting
+ """
+ previous = {}
+
+ if 'guess' in request.GET and request.GET['guess']:
+ # if there has been a previous question
+ previous = question_check_user(request, quiz, sitting) # returns a dictionary with previous question details
+ sitting.remove_first_question() # remove the first question
+
+ counter = request.session['page_count']
+ request.session['page_count'] = counter + 1 # add 1 to the page counter
+
+ qID = sitting.get_next_question()
+
+ if qID == False:
+ # no questions left
+ return final_result_user(request, sitting, previous)
+
+ show_advert = False
+
+ try:
+ if request.session['page_count'] > 0 and request.session['page_count'] % 10 == 0:
+ # advert page every 10 questions
+ counter = request.session['page_count']
+ request.session['page_count'] = counter + 1 # add 1 to the page counter
+ show_advert = True
+
+ except KeyError:
+ request.session['page_count'] = int(0) # since one hasnt been started, make it now
+
+
+ next_question = Question.objects.get(id=qID)
+
+ return render_to_response('question.html',
+ {'quiz': quiz,
+ 'question': next_question,
+ 'previous': previous,
+ 'show_advert': show_advert,
+ },
+ context_instance=RequestContext(request)
+ )
+
+
+def final_result_anon(request, quiz, previous):
+ """
+ display the outcome of the completed quiz for anon
+
+ To do:
+ if answers_at_end == True then display all questions with correct answers
+ """
+ quiz_id = str(quiz.id)
+ score = quiz_id + "_score"
+ score = request.session[score]
+ if score == 0:
+ score = "nil points"
+ max_score = quiz.question_set.all().count()
+ percent = int(round((float(score) / float(max_score)) * 100))
+
+ session_score, session_possible = anon_session_score(request)
+
+ if quiz.answers_at_end != True: # answer was shown after each question
+ return render_to_response('result.html',
+ {
+ 'score': score,
+ 'max_score': max_score,
+ 'percent': percent,
+ 'previous': previous,
+ 'session': session_score,
+ 'possible': session_possible,
+ },
+ context_instance=RequestContext(request)
+ )
+ else: # show all questions and answers
+ questions = quiz.question_set.all()
+ return render_to_response('result.html',
+ {
+ 'score': score,
+ 'max_score': max_score,
+ 'percent': percent,
+ 'questions': questions,
+ 'session': session_score,
+ 'possible': session_possible,
+ },
+ context_instance=RequestContext(request)
+ )
+
+def final_result_user(request, sitting, previous):
+ """
+ The result page for a logged in user
+ """
+ quiz = sitting.quiz
+ score = sitting.get_current_score()
+ incorrect = sitting.get_incorrect_questions()
+ max_score = quiz.question_set.all().count()
+ percent = int(round((float(score) / float(max_score)) * 100))
+
+ sitting.mark_quiz_complete() # mark as complete
+
+ if quiz.exam_paper == False: # if we do not plan to store the outcome
+ sitting.delete() # delete the sitting to free up DB space
+
+ if quiz.answers_at_end != True: # answer was shown after each question
+ return render_to_response('result.html',
+ {
+ 'quiz': quiz,
+ 'score': score,
+ 'max_score': max_score,
+ 'percent': percent,
+ 'previous': previous,
+ },
+ context_instance=RequestContext(request)
+ )
+ else: # show all questions and answers
+ questions = quiz.question_set.all()
+ return render_to_response('result.html',
+ {
+ 'quiz': quiz,
+ 'score': score,
+ 'max_score': max_score,
+ 'percent': percent,
+ 'questions': questions,
+ 'incorrect_questions': incorrect,
+ },
+ context_instance=RequestContext(request)
+ )
+
+
+def question_check_anon(request, quiz):
+ """
+ check if a question is correct, adds to score if needed and return the previous questions details
+ """
+ quiz_id = str(quiz.id)
+ guess = request.GET['guess']
+ answer = Answer.objects.get(id=guess)
+ question = answer.question # the id of the question
+
+ if answer.correct == True:
+ outcome = "correct"
+ score = quiz_id + "_score"
+ current = request.session[score]
+ current = int(current) + 1
+ request.session[score] = current # add 1 to the score
+ anon_session_score(request, 1, 1)
+
+ else:
+ outcome = "incorrect"
+ anon_session_score(request, 0, 1)
+
+ # to do - allow explanations
+ if quiz.answers_at_end != True: # display answer after each question
+ return {'previous_answer': answer, 'previous_outcome': outcome, 'previous_question': question, }
+ else: # display all answers at end
+ return {}
+
+def question_check_user(request, quiz, sitting):
+ """
+ check if a question is correct, adds to score if needed and return the previous questions details
+ """
+ quiz_id = str(quiz.id)
+ guess = request.GET['guess'] # id of the guessed answer
+ answer = Answer.objects.get(id=guess)
+ question = answer.question # the question object (only 1 question related to an answer)
+
+ if answer.correct == True:
+ outcome = "correct"
+ sitting.add_to_score(1) # add 1 to sitting score. to do: allow variable point values
+ user_progress_score_update(request, question.category, 1, 1)
+ else:
+ outcome = "incorrect"
+ sitting.add_incorrect_question(question)
+ user_progress_score_update(request, question.category, 0, 1)
+
+ # to do - allow explanations
+ if quiz.answers_at_end != True: # display answer after each question
+ return {'previous_answer': answer, 'previous_outcome': outcome, 'previous_question': question, }
+ else: # display all answers at end
+ return {}
+
+def user_progress_score_update(request, category, score, possible):
+ """
+ update the overall category score for the progress section
+ """
+ try:
+ progress = Progress.objects.get(user=request.user)
+
+ except Progress.DoesNotExist:
+ # no current progress object, make one
+ progress = Progress.objects.new_progress(request.user)
+
+ progress.update_score(category, score, possible)
+
+
+def anon_session_score(request, add=0, possible=0):
+ """
+ returns the session score
+ if number passed in then add this to the running total and then return session score
+ examples:
+ x, y = anon_session_score(1, 1) will add 1 out of a possible 1
+ x, y = anon_session_score(0, 2) will add 0 out of a possible 2
+ x, y = anon_session_score() will return the session score only without modification
+ """
+ if "session_score" not in request.session:
+ request.session["session_score"] = 0 # start total if not already running
+ request.session["session_score_possible"] = 0
+
+ if possible > 0: # if changes are due
+ score = request.session["session_score"]
+ score = score + add
+ request.session["session_score"] = score
+
+ denominator = request.session["session_score_possible"]
+ denominator = denominator + possible
+ request.session["session_score_possible"] = denominator
+
+ return request.session["session_score"], request.session["session_score_possible"]
+
+
+def progress(request):
+ """
+ displays a dashboard for the user to monitor their progress
+
+ to do:
+ progress for each category - total average score, pie chart, line graph over time
+ overall progress - current session, 7 days and 28 day line graph
+ previous practice exam results
+ """
+
+ if request.user.is_authenticated() != True: # if anon
+ # display session score and encourage to sign up
+ score, possible = anon_session_score(request)
+ return render_to_response('signup.html',
+ {'anon_score': score, 'anon_possible': possible, },
+ context_instance=RequestContext(request)
+ )
+
+ try:
+ progress = Progress.objects.get(user=request.user)
+
+
+ except Progress.DoesNotExist:
+ # viewing progress for first time. Most likely just signed up as redirect to progress after signup
+ # no current progress object, make one
+ progress = Progress.objects.new_progress(request.user)
+ return render_to_response('progress.html',
+ {'new_user': True,},
+ context_instance=RequestContext(request)
+ )
+
+ cat_scores = progress.list_all_cat_scores() # dict {category name: list of three integers [score, possible, percent]}
+
+ exams = progress.show_exams() # queryset of the exams a user has sat
+
+ return render_to_response('progress.html',
+ {'cat_scores': cat_scores, 'exams': exams},
+ context_instance=RequestContext(request)
+ )
\ No newline at end of file
--- /dev/null
+<!DOCTYPE html>
+<html xmlns:fb="http://www.facebook.com/2008/fbml" xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
+<head>
+<meta charset="utf-8">
+
+<title>Example Quiz Website | {% block title %}{% endblock %}</title>
+<meta name="description" content="{% block description %}{% endblock %}">
+
+<link rel="author" href="https://plus.google.com/u/0/XXXXXX"/>
+
+<link rel="shortcut icon" href="{{STATIC_URL}}img/favicon.ico" type="image/x-icon" />
+
+<!-- styles -->
+<link href="{{STATIC_URL}}css/XXX.css" rel="stylesheet">
+
+<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
+<!--[if lt IE 9]>
+<script src="//html5shim.googlecode.com/svn/trunk/html5.js"></script>
+<![endif]-->
+
+
+
+</head>
+
+<body>
+
+
+<!-- Before the quiz content -->
+
+
+
+{% block content %}{% endblock %}
+
+<!-- After the quiz content -->
+
+
+
+
+
+</body>
+</html>
\ No newline at end of file
--- /dev/null
+
+{% if user_was_incorrect %}
+
+ <div class="alert alert-error">
+
+ <strong>You answered the above question incorrectly</strong>
+
+ </div>
+
+
+{% endif %}
+
+<table class="table table-striped table-bordered">
+<tbody>
+{% for answer in answers %}
+
+ {% if answer.correct %}
+
+ <tr class="success">
+ <td>{{ answer.content }}</td> <td><strong>correct answer</strong></td>
+
+ {% else %}
+ <tr>
+ <td>{{ answer.content }}</td><td></td>
+
+ {% endif %}
+ </tr>
+{% endfor %}
+
+</tbody>
+</table>
\ No newline at end of file
--- /dev/null
+{% extends "base.html" %}
+
+{% load quiz_tags %}
+
+{% block title %} Progress Page {% endblock %}
+{% block description %} User Progress Page {% endblock %}
+
+{% block content %}
+
+<div class="container">
+
+{% if new_user %}
+
+ <div class="alert alert-block">
+ <button type="button" class="close" data-dismiss="alert">×</button>
+ <ul class="unstyled">
+ <li>
+ <h4>Thank you for joining this website. Welcome to your progress page.</h4>
+ </li>
+ </ul>
+ </div>
+
+{% endif %}
+
+
+
+
+{% if cat_scores %}
+
+<h1>Question Category Scores</h1>
+<p class="lead">
+ Below are the categories of questions that you have attempted. Blue is the percentage that are correct.
+</p>
+
+<script type="text/javascript" src="http://www.google.com/jsapi"></script>
+<script type="text/javascript">
+ google.load('visualization', '1', {packages: ['corechart']});
+</script>
+
+ {% for cat, value in cat_scores.items %}
+
+ {% ifnotequal cat "empty" %}
+
+ {% if forloop.first %}
+ <div class="row">
+ <ul class="thumbnails">
+ {% endif %}
+
+ {% ifequal forloop.counter 5 %}
+ </ul>
+ </div>
+ <div class="row">
+ <ul class="thumbnails">
+ {% endifequal %}
+
+ {% ifequal forloop.counter 9 %}
+ </ul>
+ </div>
+ <div class="row">
+ <ul class="thumbnails">
+ {% endifequal %}
+
+ {% ifequal forloop.counter 13 %}
+ </ul>
+ </div>
+ <div class="row">
+ <ul class="thumbnails">
+ {% endifequal %}
+
+ {% ifequal forloop.counter 17 %}
+ </ul>
+ </div>
+ <div class="row">
+ <ul class="thumbnails">
+ {% endifequal %}
+
+ <li class="span3">
+ <div class="thumbnail">
+ <script type="text/javascript">
+ function drawVisualization() {
+ // Create and populate the data table.
+ var difference = {{ value.1 }} - {{ value.0 }};
+ var correct = {{ value.0 }};
+ var data = google.visualization.arrayToDataTable([
+ ["",""],
+ ['Correct', correct],
+ ['Incorrect', difference]
+ ]);
+
+ var options = {
+ legend:{position:'none'},
+ title:"{{ cat }}",
+ fontSize: 16
+ };
+
+ // Create and draw the visualization.
+ new google.visualization.PieChart(document.getElementById('visualization{{ cat }}')).
+ draw(data, options);
+ }
+
+
+ google.setOnLoadCallback(drawVisualization);
+ </script>
+
+ <div id="visualization{{ cat }}" ></div>
+ </div>
+ </li>
+
+ {% endifnotequal %}
+
+
+ {% endfor %}
+ </ul>
+ </div>
+
+{% endif %}
+
+{% if exams %}
+
+<hr>
+
+<h1>Previous exam papers</h1>
+<p class="lead">
+ Below are the results of exams that you have sat.
+</p>
+
+<table class="table table-bordered table-striped">
+
+ <thead>
+ <tr>
+ <th>Quiz Title</th>
+ <th>Score</th>
+ <th>Possible Score</th>
+ <th>%</th>
+ </tr>
+ </thead>
+
+ <tbody>
+
+ {% for exam in exams %}
+
+ <tr>
+ {% user_previous_exam exam %}
+ </tr>
+
+ {% endfor %}
+
+ </tbody>
+
+</table>
+
+{% endif %}
+
+</div>
+
+{% endblock %}
\ No newline at end of file
--- /dev/null
+{% extends "base.html" %}
+
+{% load quiz_tags %}
+
+{% block title %} {{ quiz.title }} {% endblock %}
+{% block description %} {{ quiz.title }} - {{ quiz.description }} {% endblock %}
+
+{% block content %}
+
+<div class="container">
+
+
+{% if previous %}
+
+ <p class="muted"><small>The previous question:</small></p>
+ <p>{{ previous.previous_question }}</p>
+
+ {% ifequal previous.previous_outcome 'correct' %}
+ <div class="alert alert-success">
+ {% else %}
+ <div class="alert alert-error">
+ {% endifequal %}
+
+ <p><small>Your answer was </small><em>{{ previous.previous_answer }}</em><small> which is </small><strong>{{ previous.previous_outcome }}</strong></p>
+ </div>
+
+ {% correct_answer previous %}
+ <p><strong>Explanation:</strong></p>
+ <div class="well " style="background-color: #fcf8e3;">
+ <p>{{ previous.previous_question.explanation }}</p>
+ </div>
+
+
+<hr>
+
+{% endif %}<br />
+
+
+{% if question %}
+
+ <p><small class="muted">Question category:</small> <strong>{{ question.category }}</strong></p>
+ <p class="lead">{{ question.content }}</p>
+ {% answers_for_question question quiz %}
+
+{% endif %}
+
+ <hr>
+
+
+
+</div>
+
+{% endblock %}
--- /dev/null
+{% extends "base.html" %}
+
+{% load quiz_tags %}
+
+{% block title %} Exam Paper Result {% endblock %}
+{% block description %} Exam Results {% endblock %}
+
+{% block content %}
+
+<div class="container">
+
+{% if previous %}
+
+ <p class="muted"><small>The previous question:</small></p>
+ <p>{{ previous.previous_question }}</p>
+ <p>Your answer was <em>{{ previous.previous_answer }}</em> which is <strong>{{ previous.previous_outcome }}</strong></p>
+ {% correct_answer previous %}
+ <p><strong>Explanation:</strong></p>
+ <div class="well " style="background-color: #fcf8e3;">
+ <p>{{ previous.previous_question.explanation }}</p>
+ </div>
+<hr>
+
+{% endif %}
+
+{% if score %}
+
+ <div>
+ <h2>Exam results</h2>
+ <p><small class="muted">Exam title:</small> <strong>{{ quiz.title }}</strong></p>
+
+ <p class="lead">You answered {{ score }} questions correctly out of {{ max_score }}, giving you {{ percent }} percent correct</p>
+
+ <p>Review the questions below and try the exam again in the future.</p>
+
+ {% if user.is_authenticated %}
+
+ <p>The result of this exam will be stored in your progress section so you can review and monitor your progression.</p>
+
+ {% endif %}
+ </div>
+
+
+{% endif %}
+
+
+ <hr>
+
+{% if session and possible %}
+
+ <p class="lead">Your session score is {{ session }} out of a possible {{ possible }}</p>
+
+ <hr>
+
+{% endif %}
+
+{% if questions %}
+
+ {% for question in questions %}
+
+ <p class="lead">{{ question.content }}</p>
+ {% correct_answer_for_all_with_users_incorrect question incorrect_questions %}
+
+ {% endfor %}
+
+{% endif %}
+
+
+
+</div>
+
+{% endblock %}
\ No newline at end of file