From e5b41fd31bb8cda1929739e86a4eb09012ebd37c Mon Sep 17 00:00:00 2001 From: Tom Walker Date: Tue, 12 Mar 2013 23:15:29 +0000 Subject: [PATCH 1/1] pushing to server for the first time --- README.md | 27 ++ multichoice/__init__.py | 0 multichoice/admin.py | 0 multichoice/models.py | 52 ++++ multichoice/tests.py | 16 ++ multichoice/views.py | 1 + quiz/__init__.py | 0 quiz/admin.py | 80 ++++++ quiz/forms.py | 5 + quiz/models.py | 358 ++++++++++++++++++++++++ quiz/templatetags/__init__.py | 0 quiz/templatetags/quiz_tags.py | 43 +++ quiz/tests.py | 16 ++ quiz/urls.py | 15 + quiz/views.py | 434 +++++++++++++++++++++++++++++ templates/quiz/base.html | 41 +++ templates/quiz/correct_answer.html | 31 +++ templates/quiz/progress.html | 156 +++++++++++ templates/quiz/question.html | 53 ++++ templates/quiz/result.html | 72 +++++ 20 files changed, 1400 insertions(+) create mode 100644 README.md create mode 100644 multichoice/__init__.py create mode 100644 multichoice/admin.py create mode 100644 multichoice/models.py create mode 100644 multichoice/tests.py create mode 100644 multichoice/views.py create mode 100644 quiz/__init__.py create mode 100644 quiz/admin.py create mode 100644 quiz/forms.py create mode 100644 quiz/models.py create mode 100644 quiz/templatetags/__init__.py create mode 100644 quiz/templatetags/quiz_tags.py create mode 100644 quiz/tests.py create mode 100644 quiz/urls.py create mode 100644 quiz/views.py create mode 100644 templates/quiz/base.html create mode 100644 templates/quiz/correct_answer.html create mode 100644 templates/quiz/progress.html create mode 100644 templates/quiz/question.html create mode 100644 templates/quiz/result.html diff --git a/README.md b/README.md new file mode 100644 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 index 0000000..e69de29 diff --git a/multichoice/admin.py b/multichoice/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/multichoice/models.py b/multichoice/models.py new file mode 100644 index 0000000..56efd9f --- /dev/null +++ b/multichoice/models.py @@ -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 index 0000000..501deb7 --- /dev/null +++ b/multichoice/tests.py @@ -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 index 0000000..60f00ef --- /dev/null +++ b/multichoice/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/quiz/__init__.py b/quiz/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/quiz/admin.py b/quiz/admin.py new file mode 100644 index 0000000..2ad345e --- /dev/null +++ b/quiz/admin.py @@ -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 index 0000000..46328a0 --- /dev/null +++ b/quiz/forms.py @@ -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 index 0000000..938b3a2 --- /dev/null +++ b/quiz/models.py @@ -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 index 0000000..e69de29 diff --git a/quiz/templatetags/quiz_tags.py b/quiz/templatetags/quiz_tags.py new file mode 100644 index 0000000..cc6d2c1 --- /dev/null +++ b/quiz/templatetags/quiz_tags.py @@ -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 index 0000000..501deb7 --- /dev/null +++ b/quiz/tests.py @@ -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 index 0000000..7716fb2 --- /dev/null +++ b/quiz/urls.py @@ -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[\w-]+)/$', 'quiz.views.quiz_take'), # quiz/ + url(r'^(?P[\w-]+)$', 'quiz.views.quiz_take'), # quiz + + +) diff --git a/quiz/views.py b/quiz/views.py new file mode 100644 index 0000000..b5f79e7 --- /dev/null +++ b/quiz/views.py @@ -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 index 0000000..0f68190 --- /dev/null +++ b/templates/quiz/base.html @@ -0,0 +1,41 @@ + + + + + +Example Quiz Website | {% block title %}{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + +{% block content %}{% endblock %} + + + + + + + + + \ No newline at end of file diff --git a/templates/quiz/correct_answer.html b/templates/quiz/correct_answer.html new file mode 100644 index 0000000..0ad26d4 --- /dev/null +++ b/templates/quiz/correct_answer.html @@ -0,0 +1,31 @@ + +{% if user_was_incorrect %} + +
+ + You answered the above question incorrectly + +
+ + +{% endif %} + + + +{% for answer in answers %} + + {% if answer.correct %} + + + + + {% else %} + + + + {% endif %} + +{% endfor %} + + +
{{ answer.content }} correct answer
{{ answer.content }}
\ No newline at end of file diff --git a/templates/quiz/progress.html b/templates/quiz/progress.html new file mode 100644 index 0000000..dc52846 --- /dev/null +++ b/templates/quiz/progress.html @@ -0,0 +1,156 @@ +{% extends "base.html" %} + +{% load quiz_tags %} + +{% block title %} Progress Page {% endblock %} +{% block description %} User Progress Page {% endblock %} + +{% block content %} + +
+ +{% if new_user %} + +
+ +
    +
  • +

    Thank you for joining this website. Welcome to your progress page.

    +
  • +
+
+ +{% endif %} + + + + +{% if cat_scores %} + +

Question Category Scores

+

+ Below are the categories of questions that you have attempted. Blue is the percentage that are correct. +

+ + + + + {% for cat, value in cat_scores.items %} + + {% ifnotequal cat "empty" %} + + {% if forloop.first %} +
+
    + {% endif %} + + {% ifequal forloop.counter 5 %} +
+
+
+
    + {% endifequal %} + + {% ifequal forloop.counter 9 %} +
+
+
+
    + {% endifequal %} + + {% ifequal forloop.counter 13 %} +
+
+
+
    + {% endifequal %} + + {% ifequal forloop.counter 17 %} +
+
+
+
    + {% endifequal %} + +
  • +
    + + +
    +
    +
  • + + {% endifnotequal %} + + + {% endfor %} +
+
+ +{% endif %} + +{% if exams %} + +
+ +

Previous exam papers

+

+ Below are the results of exams that you have sat. +

+ + + + + + + + + + + + + + + {% for exam in exams %} + + + {% user_previous_exam exam %} + + + {% endfor %} + + + +
Quiz TitleScorePossible Score%
+ +{% endif %} + +
+ +{% endblock %} \ No newline at end of file diff --git a/templates/quiz/question.html b/templates/quiz/question.html new file mode 100644 index 0000000..85baa53 --- /dev/null +++ b/templates/quiz/question.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} + +{% load quiz_tags %} + +{% block title %} {{ quiz.title }} {% endblock %} +{% block description %} {{ quiz.title }} - {{ quiz.description }} {% endblock %} + +{% block content %} + +
+ + +{% if previous %} + +

The previous question:

+

{{ previous.previous_question }}

+ + {% ifequal previous.previous_outcome 'correct' %} +
+ {% else %} +
+ {% endifequal %} + +

Your answer was {{ previous.previous_answer }} which is {{ previous.previous_outcome }}

+
+ + {% correct_answer previous %} +

Explanation:

+
+

{{ previous.previous_question.explanation }}

+
+ + +
+ +{% endif %}
+ + +{% if question %} + +

Question category: {{ question.category }}

+

{{ question.content }}

+ {% answers_for_question question quiz %} + +{% endif %} + +
+ + + +
+ +{% endblock %} diff --git a/templates/quiz/result.html b/templates/quiz/result.html new file mode 100644 index 0000000..2cb671e --- /dev/null +++ b/templates/quiz/result.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} + +{% load quiz_tags %} + +{% block title %} Exam Paper Result {% endblock %} +{% block description %} Exam Results {% endblock %} + +{% block content %} + +
+ +{% if previous %} + +

The previous question:

+

{{ previous.previous_question }}

+

Your answer was {{ previous.previous_answer }} which is {{ previous.previous_outcome }}

+ {% correct_answer previous %} +

Explanation:

+
+

{{ previous.previous_question.explanation }}

+
+
+ +{% endif %} + +{% if score %} + +
+

Exam results

+

Exam title: {{ quiz.title }}

+ +

You answered {{ score }} questions correctly out of {{ max_score }}, giving you {{ percent }} percent correct

+ +

Review the questions below and try the exam again in the future.

+ + {% if user.is_authenticated %} + +

The result of this exam will be stored in your progress section so you can review and monitor your progression.

+ + {% endif %} +
+ + +{% endif %} + + +
+ +{% if session and possible %} + +

Your session score is {{ session }} out of a possible {{ possible }}

+ +
+ +{% endif %} + +{% if questions %} + + {% for question in questions %} + +

{{ question.content }}

+ {% correct_answer_for_all_with_users_incorrect question incorrect_questions %} + + {% endfor %} + +{% endif %} + + + +
+ +{% endblock %} \ No newline at end of file -- 2.39.5