]> git.parisson.com Git - django_quiz.git/commitdiff
pushing to server for the first time
authorTom Walker <tomwalker0472@gmail.com>
Tue, 12 Mar 2013 23:15:29 +0000 (23:15 +0000)
committerTom Walker <tomwalker0472@gmail.com>
Tue, 12 Mar 2013 23:15:29 +0000 (23:15 +0000)
20 files changed:
README.md [new file with mode: 0644]
multichoice/__init__.py [new file with mode: 0644]
multichoice/admin.py [new file with mode: 0644]
multichoice/models.py [new file with mode: 0644]
multichoice/tests.py [new file with mode: 0644]
multichoice/views.py [new file with mode: 0644]
quiz/__init__.py [new file with mode: 0644]
quiz/admin.py [new file with mode: 0644]
quiz/forms.py [new file with mode: 0644]
quiz/models.py [new file with mode: 0644]
quiz/templatetags/__init__.py [new file with mode: 0644]
quiz/templatetags/quiz_tags.py [new file with mode: 0644]
quiz/tests.py [new file with mode: 0644]
quiz/urls.py [new file with mode: 0644]
quiz/views.py [new file with mode: 0644]
templates/quiz/base.html [new file with mode: 0644]
templates/quiz/correct_answer.html [new file with mode: 0644]
templates/quiz/progress.html [new file with mode: 0644]
templates/quiz/question.html [new file with mode: 0644]
templates/quiz/result.html [new file with mode: 0644]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..bd5b21f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+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
diff --git a/multichoice/__init__.py b/multichoice/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/multichoice/admin.py b/multichoice/admin.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/multichoice/models.py b/multichoice/models.py
new file mode 100644 (file)
index 0000000..56efd9f
--- /dev/null
@@ -0,0 +1,52 @@
+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
diff --git a/multichoice/tests.py b/multichoice/tests.py
new file mode 100644 (file)
index 0000000..501deb7
--- /dev/null
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.assertEqual(1 + 1, 2)
diff --git a/multichoice/views.py b/multichoice/views.py
new file mode 100644 (file)
index 0000000..60f00ef
--- /dev/null
@@ -0,0 +1 @@
+# Create your views here.
diff --git a/quiz/__init__.py b/quiz/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quiz/admin.py b/quiz/admin.py
new file mode 100644 (file)
index 0000000..2ad345e
--- /dev/null
@@ -0,0 +1,80 @@
+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
diff --git a/quiz/forms.py b/quiz/forms.py
new file mode 100644 (file)
index 0000000..46328a0
--- /dev/null
@@ -0,0 +1,5 @@
+from django import forms
+from django.forms.models import inlineformset_factory
+
+from multichoice.models import Question, Answer
+
diff --git a/quiz/models.py b/quiz/models.py
new file mode 100644 (file)
index 0000000..938b3a2
--- /dev/null
@@ -0,0 +1,358 @@
+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
diff --git a/quiz/templatetags/__init__.py b/quiz/templatetags/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quiz/templatetags/quiz_tags.py b/quiz/templatetags/quiz_tags.py
new file mode 100644 (file)
index 0000000..cc6d2c1
--- /dev/null
@@ -0,0 +1,43 @@
+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
diff --git a/quiz/tests.py b/quiz/tests.py
new file mode 100644 (file)
index 0000000..501deb7
--- /dev/null
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.assertEqual(1 + 1, 2)
diff --git a/quiz/urls.py b/quiz/urls.py
new file mode 100644 (file)
index 0000000..7716fb2
--- /dev/null
@@ -0,0 +1,15 @@
+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
+
+
+)
diff --git a/quiz/views.py b/quiz/views.py
new file mode 100644 (file)
index 0000000..b5f79e7
--- /dev/null
@@ -0,0 +1,434 @@
+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
diff --git a/templates/quiz/base.html b/templates/quiz/base.html
new file mode 100644 (file)
index 0000000..0f68190
--- /dev/null
@@ -0,0 +1,41 @@
+<!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
diff --git a/templates/quiz/correct_answer.html b/templates/quiz/correct_answer.html
new file mode 100644 (file)
index 0000000..0ad26d4
--- /dev/null
@@ -0,0 +1,31 @@
+
+{% 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
diff --git a/templates/quiz/progress.html b/templates/quiz/progress.html
new file mode 100644 (file)
index 0000000..dc52846
--- /dev/null
@@ -0,0 +1,156 @@
+{% 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">&times;</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
diff --git a/templates/quiz/question.html b/templates/quiz/question.html
new file mode 100644 (file)
index 0000000..85baa53
--- /dev/null
@@ -0,0 +1,53 @@
+{% 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 %}
diff --git a/templates/quiz/result.html b/templates/quiz/result.html
new file mode 100644 (file)
index 0000000..2cb671e
--- /dev/null
@@ -0,0 +1,72 @@
+{% 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