from model_utils.managers import InheritanceManager
# the above taken from:
-# https://django-model-utils.readthedocs.org/en/latest/managers.html#inheritancemanager
+# https://django-model-utils.readthedocs.org/en/latest/managers.html
"""
-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
+If you want to prepopulate the category choices then here is an example.
+Uncomment 'choices' in the category model.
"""
CATEGORY_CHOICES = ( ('Endocrinology', 'Endocrinology'),
class Category(models.Model):
- category = models.CharField(max_length=250,
- blank=True,
- # choices=CATEGORY_CHOICES,
- unique=True,
- null=True,
- )
+ category = models.CharField(max_length = 250,
+ blank = True,
+ # choices = CATEGORY_CHOICES,
+ unique = True,
+ null = True,)
class Meta:
def __unicode__(self):
return self.category
+
"""
-Quiz is a container that can be filled with various different question types
-or other content
+Quiz is a container that can be filled with various different question types.
"""
class Quiz(models.Model):
- title = models.CharField(max_length=60,
- blank=False,
- )
+ title = models.CharField(max_length = 60,
+ blank = False,)
- description = models.TextField(blank=True,
- help_text="a description of the quiz",
- )
+ 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,
- )
-
- 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):
+ 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):
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
def __unicode__(self):
- return self.title
+ return self.title[:25]
"""
-Progress is used to track an individual signed in users score on different quiz's and categories
+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
- """
+
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 = 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, ...]
+ Currently stores the score for each category, max possible they could
+ have got, and previous exam paper scores.
+ Data stored in csv using the format:
+ category, score, possible, category, score, possible, ...
"""
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
+ score = models.TextField() # The god awful csv.
+ # 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,
+ 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 dict will have one key for every category that you have defined.
+
+ The dict will have one key for every category that you have defined
+ BUT
"""
- categories = Category.objects.all() # all the categories possible
- score_before = self.score # copy the original score csv to use later....
+ categories = Category.objects.all()
+ score_before = self.score
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)
+ for cat in categories:
+ to_find = re.escape(cat.category) + r",(\d+),(\d+),"
+ # group 1 is score, group 2 is possible
+
+ match = re.search(to_find, self.score, re.IGNORECASE)
if match:
score = int(match.group(1))
possible = int(match.group(2))
+
try:
percent = int(round((float(score) / float(possible)) * 100))
except:
percent = 0
+
score_list = [score, possible, percent]
output[cat.category] = score_list
- else: # Is possible to remove/comment this section out
- 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
+ else: # if category has not been added yet, add it.
+ temp = self.score + cat.category + ",0,0,"
self.score = temp
output[cat.category] = [0, 0]
- if len(self.score) > len(score_before): # if changes have been made
- self.save() # save only at the end to minimise disc writes
+ if len(self.score) > len(score_before):
+ """
+ If a new category has been added, save changes. Otherwise nothing
+ will be saved.
+ """
+ self.save()
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
+ Pass in a category, get the users score and possible maximum score
+ as the integers x,y respectively
"""
- category_test = Category.objects.filter(category=category_queried).exists()
+ category_test = Category.objects.filter(category = category_queried) \
+ .exists()
if category_test == False:
- return "error", "category does not exist" # to do: make this useful!
+ return "error", "category does not exist"
- my_regex = re.escape(category_queried) + r",(\d+),(\d+)," # group 1 is score, group 2 is possible
+ to_find = re.escape(category_queried) + r",(\d+),(\d+),"
- match = re.search(my_regex, self.score, re.IGNORECASE)
+ match = re.search(to_find, 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
+ else: # if not found but category exists, add category with 0 points
+ temp = self.score + category_queried + ",0,0,"
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
+ Pass in category, amount to increase score and max possible
+ increase if all were correct.
- data stored in csv using the format [category, score, possible, category, score, possible, ...]
+ Does not return anything.
"""
- category_test = Category.objects.filter(category=category_queried).exists()
+ category_test = Category.objects.filter(category = category_queried) \
+ .exists()
if category_test == False:
- return "error", "category does not exist" # to do: make useful
+ return "error", "category does not exist"
- my_regex = re.escape(str(category_queried)) + r",(\d+),(\d+)," # group 1 is score, group 2 is possible
+ to_find = re.escape(str(category_queried)) + r",(\d+),(\d+),"
- match = re.search(my_regex, self.score, re.IGNORECASE)
+ match = re.search(to_find, 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 # add on the score
- updated_current_possible = current_possible + possible_to_add # add the possible maximum score
+ 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) + ","
+ 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
+ # swap old score for the new one
+ self.score = temp.replace(found_instance, new_score)
self.save()
-
else:
"""
- if not present but a verified category, add with the points passed in
+ if not present but existing category, add with the points passed in
"""
- temp = self.score
- temp = temp + str(category_queried) + "," + str(score_to_add) + "," + str(possible_to_add) + ","
+ temp = (self.score +
+ 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 complete exams
+ Finds the previous quizzes marked as 'exam papers'.
+ Returns a queryset of complete exams.
"""
- # list of exam objects from user that are complete
- return Sitting.objects.filter(user=self.user).filter(complete=True)
+ return Sitting.objects.filter(user = self.user) \
+ .filter(complete = True)
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('?')
+ question_set = quiz.question_set.all() \
+ .select_subclasses() \
+ .order_by('?')
else:
- question_set = quiz.question_set.all()
+ question_set = quiz.question_set.all() \
+ .select_subclasses()
questions = ""
for question in question_set:
- questions = questions + str(question.id) + "," # string of IDs seperated by commas
+ questions = (questions + str(question.id) + ",")
- new_sitting = self.create(user=user,
- quiz=quiz,
+ new_sitting = self.create(user = user,
+ quiz = quiz,
question_list = questions,
incorrect_questions = "",
- current_score="0",
- complete=False,
- )
+ 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
+ Used to store the progress of logged in users sitting a quiz.
+ Replaces the session system used by anon users.
- 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.
+ Question_list is a list of integers which represent id's of
+ the unanswered questions in csv format.
- incorrect_questions is a list of id's of the questions answered wrongly
+ Incorrect_questions is a list in the same format.
- 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
+ Sitting deleted when quiz finished unless quiz.exam_paper is true.
"""
- user = models.ForeignKey('auth.User') # one user per exam class
+ user = models.ForeignKey('auth.User')
quiz = models.ForeignKey(Quiz)
- question_list = models.TextField() # another awful csv. Always end with a comma
+ question_list = models.TextField()
- incorrect_questions = models.TextField(blank=True) # more awful csv. Always end with a comma
+ incorrect_questions = models.TextField(blank = True)
- current_score = models.TextField() # a string of the score ie 19 convert to int for use
+ current_score = models.IntegerField()
- complete = models.BooleanField(default=False, blank=False)
+ complete = models.BooleanField(default = False, blank = False)
objects = SittingManager()
def get_next_question(self):
"""
- Returns the next question ID (as an integer).
+ Returns integer of the next question ID.
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
+ first_comma = self.question_list.find(',')
+ if first_comma == -1 or first_comma == 0:
return False
- qID = self.question_list[:first_comma] # up to but not including the first comma
+ return self.question_list[: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()
+ first_comma = self.question_list.find(',')
+ if first_comma != -1 or first_comma != 0:
+ self.question_list = self.question_list[first_comma+1:]
+ self.save()
def add_to_score(self, points):
- """
- Adds the points to the running total.
- Does not return anything
- """
present_score = self.get_current_score()
updated_score = present_score + int(points)
- self.current_score = str(updated_score)
+ self.current_score = updated_score
self.save()
def get_current_score(self):
- """
- returns the current score as an integer
- """
- return int(self.current_score)
+ return 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))
+ dividend = float(self.current_score)
+ divisor = self.quiz.question_set.all().select_subclasses().count()
+ if divisor < 1:
+ return 0 # prevent divide by zero error
+
+ correct = int(round((dividend / divisor) * 100))
+
+ if correct >= 1:
+ return correct
+ else:
+ return 0
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
- The question object must be passed in
- Does not return anything
+ Adds uid of incorrect question to the list.
+ The question object must be passed in.
"""
current_incorrect = self.incorrect_questions
question_id = question.id
- if current_incorrect == "":
- updated = str(question_id) + ","
- else:
- updated = current_incorrect + str(question_id) + ","
- self.incorrect_questions = updated
+
+ self.incorrect_questions = current_incorrect + str(question_id) + ","
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 strings ie [32,19,22,3,75]
+ question_list = self.incorrect_questions
+ split_questions = question_list.split(',')
return split_questions
of managers as one isn't created.
"""
- quiz = models.ManyToManyField(Quiz, blank=True, )
+ quiz = models.ManyToManyField(Quiz, blank = True, )
- category = models.ForeignKey(Category, blank=True, null=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',
+ 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',
+ explanation = models.TextField(max_length = 2000,
+ blank = True,
+ help_text = "Explanation to be shown after the question has been answered.",
+ verbose_name = 'Explanation',
)
objects = InheritanceManager()