+import re # uh oh
+
from django.db import models
-from django.utils.encoding import smart_str
from django.conf import settings
+from django.utils.encoding import smart_str
from django.contrib.auth.models import User
+"""
+If you want to prepopulate the category choices then use the following and uncomment 'choices' in the category model
+I have left in my original set as an example
+"""
+
+CATEGORY_CHOICES = ( ('Endocrinology', 'Endocrinology'),
+ ('Dermatology', 'Dermatology'),
+ ('Cellular Biology', 'Cellular Biology'),
+ ('Neurology', 'Neurology'),
+ ('Gastroenterology', 'Gastroenterology'),
+ ('Statistics', 'Statistics'),
+ ('Rheumatology', 'Rheumatology'),
+ ('Tropical medicine', 'Tropical medicine'),
+ ('Respiratory', 'Respiratory'),
+ ('Immunology', 'Immunology'),
+ ('Nephrology', 'Nephrology'),
+ ('Genetic Medicine', 'Genetic Medicine'),
+ ('Haematology', 'Haematology'),
+ ('Pharmacology', 'Pharmacology'),
+ ('Physiology', 'Physiology'),
+ ('Ophthalmology', 'Ophthalmology'),
+ ('Anatomy', 'Anatomy'),
+ ('Biochemistry', 'Biochemistry'),
+ ('empty', 'empty'),
+ ('Psychiatry', 'Psychiatry'),
+ ('Cardiology', 'Cardiology'),
+ )
"""
-Quiz is a container that can be filled with various different question types
-or other content
+Category used to define a category for either a quiz or question
"""
-
class CategoryManager(models.Manager):
"""
custom manager for Progress class
category = models.CharField(max_length=250,
blank=True,
- choices=CATEGORY_CHOICES,
+ # choices=CATEGORY_CHOICES,
unique=True,
null=True,
)
def __unicode__(self):
return self.category
+"""
+Quiz is a container that can be filled with various different question types
+or other content
+"""
class Quiz(models.Model):
- title = models.CharField(max_length=60)
+ title = models.CharField(max_length=60,
+ blank=False,
+ )
- description = models.TextField(blank=True)
+ description = models.TextField(blank=True,
+ help_text="a description of the quiz",
+ )
- url = models.CharField(max_length=60,
- blank=False,
- help_text="an SEO friendly url",
- verbose_name='SEO friendly url',
- )
+ 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, )
+ category = models.ForeignKey(Category,
+ null=True,
+ blank=True,
+ )
random_order = models.BooleanField(blank=False,
default=False,
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",
- )
+ 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()
+ def save(self, force_insert=False, force_update=False):
+ self.url = self.url.replace(' ', '-').lower() # automatically converts url to lowercase, replace space with dash
+ self.url = ''.join(letter for letter in self.url if letter.isalnum() or letter == '-') # removes non-alphanumerics
super(Quiz, self).save(force_insert, force_update)
return self.title
+"""
+Progress is used to track an individual signed in users score on different quiz's and categories
+"""
+
+
class ProgressManager(models.Manager):
"""
custom manager for Progress class
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
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
+ the third is the percentage correct.
+ The dict will have one key for every category that you have defined.
"""
- import re # uh oh
categories = Category.objects.all() # all the categories possible
+ score_before = self.score # copy the original score csv to use later....
output = {}
for cat in categories: # for each of the categories
else: # Is possible to remove/comment this section out
- temp = self.score
- temp = temp + cat.category + ",0,0," # always end with a comma
+ temp = self.score # temporarily store the current csv that lists all the scores
+ temp = temp + cat.category + ",0,0," # Add the class that is not listed at the end. Always end with a comma
self.score = temp
output[cat.category] = [0, 0]
- self.save() # add this new output to disk
+
+
+ if len(self.score) > len(score_before): # if changes have been made
+ self.save() # save only at the end to minimise disc writes
return output
category_test = Category.objects.filter(category=category_queried).exists()
if category_test == False:
- return "error", "category does not exist" # to do: update
+ return "error", "category does not exist" # to do: make this useful!
- 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)
category_test = Category.objects.filter(category=category_queried).exists()
if category_test == False:
- return "error", "category does not exist" # to do: update
+ return "error", "category does not exist" # to do: make useful
- 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)
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
+ updated_current_score = current_score + score_to_add # add on the score
+ updated_current_possible = current_possible + possible_to_add # add the possible maximum score
new_score = str(category_queried) + "," + str(updated_current_score) + "," + str(updated_current_possible) + ","
temp = self.score
temp = temp + str(category_queried) + "," + str(score_to_add) + "," + str(possible_to_add) + ","
self.score = temp
- self.save()
-
+ self.save()
+
- def show_exams(self,):
+ def show_exams(self):
"""
finds the previous exams marked as 'exam papers'
"""
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):
"""
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
+
+ question_list is a list of id's of the unanswered questions. Stored as a textfield to allow >255 chars. quesion_list
+ is in csv format.
+
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
+
+ current_Score is a total of the 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 true, or DB will swell quickly in size
"""
user = models.ForeignKey('auth.User') # one user per exam class
current_score = models.TextField() # a string of the score ie 19 convert to int for use
- complete = models.BooleanField(default=False,)
+ complete = models.BooleanField(default=False, blank=False)
objects = SittingManager()
def get_next_question(self):
"""
Returns the next question ID (as an integer).
+ If no question is found, returns False
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 add_to_score(self, points):
"""
- Adds the input (points) to the running total.
+ Adds the 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)
+ updated_score = present_score + int(points)
+ self.current_score = str(updated_score)
+ self.save()
def get_current_score(self):
"""
"""
return int(self.current_score)
+ def get_percent_correct(self):
+ """
+ returns the percentage correct as an integer
+ """
+ return int(round((float(self.current_score) / float(self.quiz.question_set.all().count())) * 100))
+
def mark_quiz_complete(self):
"""
Changes the quiz to complete.
def add_incorrect_question(self, question):
"""
Adds the uid of an incorrect question to the list of incorrect questions
+ The question object must be passed in
Does not return anything
"""
- current = self.incorrect_questions
+ current_incorrect = self.incorrect_questions
question_id = question.id
- if current == "":
+ if current_incorrect == "":
updated = str(question_id) + ","
else:
- updated = current + str(question_id) + ","
+ updated = current_incorrect + str(question_id) + ","
self.incorrect_questions = updated
self.save()
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]
+ split_questions = question_list.split(',') # list of strings ie [32,19,22,3,75]
return split_questions
\ No newline at end of file
-from django.http import HttpResponse
-from django.shortcuts import render_to_response
+import random
+
from django.core.exceptions import ObjectDoesNotExist
-from django.contrib import auth
from django.core.context_processors import csrf
+from django.contrib import auth
+from django.http import HttpResponse
from django.http import HttpResponseRedirect
+from django.shortcuts import render_to_response
from django.template import RequestContext
-
from quiz.models import Quiz, Category, Progress, Sitting
from multichoice.models import Question, Answer
***********************************
-
-used by anon only:
+used by anonymous (non-logged in) users 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
+ request.session['page_count'] is a counter used for displaying message every X number of pages
+
+useful query sets:
-question.answer_set.all() is all the answers for question
-quiz.question_set.all() is all the questions in a quiz
+ 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
+ allow the page count before a message is shown to be set in admin
"""
quiz=quiz,
complete=False,
)[0] # use the first one
+
return user_load_next_question(request, previous_sitting, quiz)
else:
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
+ Sets the session variables when starting a quiz for the first time when not logged in
"""
-
+
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
+ if 'page_count' not in request.session:
+ request.session['page_count'] = int(0) # session page count for adverts
return load_anon_next_question(request, quiz)
"""
sitting = Sitting.objects.new_sitting(request.user, quiz)
- request.session['page_count'] = int(0) # session page count for adverts
+ if 'page_count' not in request.session:
+ request.session['page_count'] = int(0) # session page count for adverts
return user_load_next_question(request, sitting, quiz)
request.session[q_list] = question_list
counter = request.session['page_count']
- request.session['page_count'] = counter + 1 # add 1 to the page counter
-
-
+ request.session['page_count'] = counter + 1 # add 1 to the page counter
if not request.session[q_list]:
# no questions left!
next_question_id = question_list[0]
question = Question.objects.get(id=next_question_id)
-
-
+
return render_to_response('question.html',
{'quiz': quiz,
'question': question,
counter = request.session['page_count']
request.session['page_count'] = counter + 1 # add 1 to the page counter
- qID = sitting.get_next_question()
+ question_ID = sitting.get_next_question()
- if qID == False:
+ if question_ID == False:
# no questions left
return final_result_user(request, sitting, previous)
request.session['page_count'] = int(0) # since one hasnt been started, make it now
- next_question = Question.objects.get(id=qID)
+ next_question = Question.objects.get(id=question_ID)
return render_to_response('question.html',
{'quiz': quiz,
quiz_id = str(quiz.id)
score = quiz_id + "_score"
score = request.session[score]
+ percent = int(round((float(score) / float(max_score)) * 100))
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
+ if quiz.answers_at_end != True: # if answer was shown after each question
return render_to_response('result.html',
{
'score': score,
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))
+ percent = sitting.get_percent_correct()
sitting.mark_quiz_complete() # mark as complete
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
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