From 2f9a9b9ed0fb1af6870a3e2ec75853789c75d029 Mon Sep 17 00:00:00 2001 From: Tom Walker Date: Thu, 12 Jun 2014 23:03:56 +0100 Subject: [PATCH] Working mainly on quiz.models to allow for any question type to be used by rest of model apparatus. Also little improvements to make code more maintainable. --- quiz/models.py | 361 +++++++++++++++++------------------ quiz/views.py | 6 +- templates/quiz/question.html | 2 + 3 files changed, 175 insertions(+), 194 deletions(-) diff --git a/quiz/models.py b/quiz/models.py index 9b2aa42..6c743b9 100644 --- a/quiz/models.py +++ b/quiz/models.py @@ -7,12 +7,12 @@ from django.contrib.auth.models import User 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'), @@ -58,12 +58,11 @@ class CategoryManager(models.Manager): 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: @@ -73,54 +72,47 @@ class Category(models.Model): 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 @@ -134,37 +126,35 @@ class Quiz(models.Model): 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() @@ -174,264 +164,253 @@ class Progress(models.Model): 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 @@ -443,20 +422,20 @@ class Question(models.Model): 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() diff --git a/quiz/views.py b/quiz/views.py index fed4282..be829d1 100644 --- a/quiz/views.py +++ b/quiz/views.py @@ -186,12 +186,12 @@ def load_anon_next_question(request, quiz): # 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) + next_question = Question.objects.get_subclass(id = next_question_id) question_type = next_question.__class__.__name__ return render_to_response('question.html', {'quiz': quiz, - 'question': question, + 'question': next_question, 'question_type': question_type, 'previous': previous, 'show_advert': show_advert, @@ -233,7 +233,7 @@ def user_load_next_question(request, sitting, quiz): # request.session['page_count'] = int(0) # since one hasnt been started, make it now - next_question = Question.objects.get(id=question_ID) + next_question = Question.objects.get_subclass(id = next_question_id) question_type = next_question.__class__.__name__ return render_to_response('question.html', diff --git a/templates/quiz/question.html b/templates/quiz/question.html index ea07a32..d9ee9b8 100644 --- a/templates/quiz/question.html +++ b/templates/quiz/question.html @@ -41,6 +41,8 @@ {% if question %}

Question category: {{ question.category }}

+

{{ question.id }}

+

{{ question_type }}

{{ question.content }}

{% answers_for_question question quiz %} -- 2.39.5