From: Tom Walker Date: Tue, 10 Jun 2014 21:59:40 +0000 (+0100) Subject: changes to allow easier creation of new question types, updated admin to match X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=dadaf167a115cfce95b5ef14282f11631597b81f;p=django_quiz.git changes to allow easier creation of new question types, updated admin to match --- diff --git a/multichoice/models.py b/multichoice/models.py index 738a851..8ed98d2 100644 --- a/multichoice/models.py +++ b/multichoice/models.py @@ -1,42 +1,23 @@ from django.db import models -from quiz.models import Quiz, Category +from quiz.models import Quiz, Category, Question """ 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 MCQuestion(Question): + """ + Everything inherited from base question class. + """ class Meta: - verbose_name = "Question" - verbose_name_plural = "Questions" - ordering = ['category'] + verbose_name = "Multiple Choice Question" + verbose_name_plural = "Multiple Choice Questions" - def __unicode__(self): - return self.content - class Answer(models.Model): - question = models.ForeignKey(Question) + question = models.ForeignKey(MCQuestion) content = models.CharField(max_length=1000, blank=False, diff --git a/quiz/admin.py b/quiz/admin.py index 6fa7afd..97ebe7b 100644 --- a/quiz/admin.py +++ b/quiz/admin.py @@ -1,8 +1,9 @@ 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 + +from quiz.models import Quiz, Category, Progress, Question +from multichoice.models import MCQuestion, Answer from true_false.models import TF_Question class QuestionInline(admin.TabularInline): @@ -24,10 +25,10 @@ class QuizAdminForm(forms.ModelForm): model = Quiz questions = forms.ModelMultipleChoiceField( - queryset=Question.objects.all(), - required=False, - widget=FilteredSelectMultiple(verbose_name=('Questions'), - is_stacked=False)) + queryset = Question.objects.all().select_subclasses(), + required = False, + widget = FilteredSelectMultiple(verbose_name = ('Questions'), + is_stacked = False)) def __init__(self, *args, **kwargs): super(QuizAdminForm, self).__init__(*args, **kwargs) @@ -55,7 +56,7 @@ class QuizAdmin(admin.ModelAdmin): class CategoryAdmin(admin.ModelAdmin): search_fields = ('category', ) -class QuestionAdmin(admin.ModelAdmin): +class MCQuestionAdmin(admin.ModelAdmin): list_display = ('content', 'category', ) list_filter = ('category',) fields = ('content', 'category', 'quiz', 'explanation' ) @@ -77,13 +78,13 @@ class ProgressAdmin(admin.ModelAdmin): class TFQuestionAdmin(admin.ModelAdmin): list_display = ('content', 'category', ) list_filter = ('category',) - fields = ('content', 'category', 'quiz', 'explanation' ) + fields = ('content', 'category', 'quiz', 'explanation', 'correct',) search_fields = ('content', 'explanation') filter_horizontal = ('quiz',) admin.site.register(Quiz, QuizAdmin) admin.site.register(Category, CategoryAdmin) -admin.site.register(Question, QuestionAdmin) +admin.site.register(MCQuestion, MCQuestionAdmin) admin.site.register(Progress, ProgressAdmin) admin.site.register(TF_Question, TFQuestionAdmin) diff --git a/quiz/models.py b/quiz/models.py index 5282230..8cb6364 100644 --- a/quiz/models.py +++ b/quiz/models.py @@ -2,12 +2,17 @@ import re # uh oh from django.db import models from django.conf import settings -from django.utils.encoding import smart_str +from django.utils.encoding import smart_str 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 + + """ 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 +I have left in my original set as an example """ CATEGORY_CHOICES = ( ('Endocrinology', 'Endocrinology'), @@ -41,28 +46,28 @@ Category used to define a category for either a quiz or question 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() + new_category.save() class Category(models.Model): - - category = models.CharField(max_length=250, - blank=True, + + 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 @@ -72,38 +77,38 @@ or other content """ class Quiz(models.Model): - + title = models.CharField(max_length=60, blank=False, ) - + 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', ) - - category = models.ForeignKey(Category, - null=True, + + 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\ @@ -111,13 +116,13 @@ class Quiz(models.Model): ) - def save(self, force_insert=False, force_update=False): + 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) @@ -128,7 +133,7 @@ class Quiz(models.Model): def __unicode__(self): return self.title - + """ Progress is used to track an individual signed in users score on different quiz's and categories @@ -138,7 +143,7 @@ Progress is used to track an individual signed in users score on different quiz' 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 @@ -150,34 +155,37 @@ class ProgressManager(models.Manager): 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, ...] - + """ - + 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() - - + + class Meta: + verbose_name = "User Progress" + verbose_name_plural = "User progress records" + 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. + 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. """ - + categories = Category.objects.all() # all the categories possible score_before = self.score # copy the original score csv to use later.... - output = {} - + 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)) @@ -187,42 +195,42 @@ class Progress(models.Model): 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 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 - + 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: make this useful! - + 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 @@ -230,44 +238,44 @@ class Progress(models.Model): 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, ...] - """ - + + 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: make useful - + 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 # 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 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 @@ -276,12 +284,12 @@ class Progress(models.Model): 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 complete exams """ # list of exam objects from user that are complete @@ -291,7 +299,7 @@ class Progress(models.Model): 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 @@ -300,17 +308,17 @@ class SittingManager(models.Manager): 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, + current_score="0", + complete=False, ) new_sitting.save() return new_sitting @@ -319,33 +327,33 @@ class SittingManager(models.Manager): 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 - + 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 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 + + 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 - + 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, blank=False) - + objects = SittingManager() - + def get_next_question(self): """ Returns the next question ID (as an integer). @@ -355,11 +363,11 @@ class Sitting(models.Model): 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. @@ -370,7 +378,7 @@ class Sitting(models.Model): 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 points to the running total. @@ -380,19 +388,19 @@ class Sitting(models.Model): updated_score = present_score + int(points) self.current_score = str(updated_score) self.save() - + def get_current_score(self): """ returns the current score as an integer """ 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. @@ -400,7 +408,7 @@ class Sitting(models.Model): """ self.complete = True self.save() - + def add_incorrect_question(self, question): """ Adds the uid of an incorrect question to the list of incorrect questions @@ -415,7 +423,7 @@ class Sitting(models.Model): updated = current_incorrect + 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 @@ -423,3 +431,38 @@ class Sitting(models.Model): 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] return split_questions + + +class Question(models.Model): + """ + Base class for all question types. + Shared properties placed here. + Was originally going to be an abstract class but limits the use + of managers as one isn't created. + """ + + 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', + ) + + objects = InheritanceManager() + + class Meta: + ordering = ['category'] + + + + def __unicode__(self): + return self.content diff --git a/true_false/models.py b/true_false/models.py index d35d059..a55a6f8 100644 --- a/true_false/models.py +++ b/true_false/models.py @@ -18,10 +18,10 @@ class TF_Question(Question): ) class Meta: - verbose_name = "Question" - verbose_name_plural = "Questions" + verbose_name = "True/False Question" + verbose_name_plural = "True/False Questions" ordering = ['category'] def __unicode__(self): - return self.content + return self.content[:50]