Skip to content

Commit 65ab55a

Browse files
authored
Merge pull request #149 from codersforcauses/i144-update_statistics_to_match_new_scope
I144 update statistics to match new scope
2 parents 5c59119 + f6211fd commit 65ab55a

11 files changed

Lines changed: 226 additions & 56 deletions

File tree

documentation/docs/backend/quiz/shared_models.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@ The `shared_models` app is designed to hold the models and serializers that need
88

99
### Quiz
1010

11-
There are three shared quiz models:
11+
There are four shared quiz models:
1212

1313
* Question - a model representing a generic quiz question. Questions come in three types (represented by the `question_type` field): multiple choice, numerical answer and short answer.
14-
* Tag - a model representing a generic tag which can be attached to many Questions (and each Question may have many associated Tags). Example Tags would include the subject and unit of study the question pertains to, as well as the topic within that subject/unit. Tags will be able to be created and modified by admins for regular users to use to tag their questions.
14+
* Topic - a model representing a specific subject topic. Topics can be attached to many Questions (and each Question may have many associated topics). topics will be able to be created and modified by admins for regular users to use to tag their questions.
15+
* Subject - a model representing a school subject. There can be many topics for one subject. Subjects are not connected to questions, as it is simpler for the backend to derive the subjects from the topics instead of having both topics and subjects connected to a question which complicates things. Subjects can also be created and modified by admins.
1516
* Answer - a model representing an answer associated with a particular Question. Each Question can have multiple associated answers. Each answer can be marked as being correct or incorrect. For the three different types of Question, the intended use of the Answer model varies:
1617
* For multiple choice questions, each Answer will appear as an option for the user taking the quiz to select. One or more Answers will be correct, and the remaining Answers will be incorrect.
1718
* For numerical answer questions, no options will be displayed, and the user must enter a numerical value into a text field. Each of the Answers associated with such a Question should have `is_correct = True`, and if the user's input matches any of these answers, they will be marked correct. Answers which have `is_correct = False` will not do anything and hence **should not be added** to numerical answer questions.
1819
* For short answer questions, the user will be given an extended text field to enter their answer into. Due to the nature of this question, automated marking is not possible, and so the user will have to self-mark their response. Upon submitting their answer, all Answers associated to the Question with `is_correct = True` will be displayed for the user to compare their response to. As with numerical answer questions, Answers with `is_correct = False` will not do anything and hence **should not be added**.
1920

2021
### Statistics
2122

22-
* QuestionResponse - a model representing a response to a Question submitted by a User, stored in the database for statistics purposes. The QuestionResponse model contains all of the necessary data for the following statistics to be surmised, as per the client's requirements:
23-
* How long the User took to complete the Question
24-
* How many Users in total have attempted a given Question
25-
* How many Users have gotten the Question right and how many have gotten it wrong
23+
* QuizStatistics - a model representing a response to a Quiz submitted by a User, stored in the database for statistics purposes. The QuizStatistics model contains all of the necessary data for the following statistics to be surmised, as per the client's requirements:
24+
* How many questions the quiz has
25+
* Total marks and the user's mark
26+
* How long the User took to complete the Quiz
27+
* When the quiz was taken
28+
* How many Users in total have attempted a given Quiz
2629
* The average score (across all Users) for a given Question
2730
* The average score (across all Users) for a given Tag (i.e. subject and topic)
2831

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
from django.contrib import admin
2-
from .models.quiz_models import Question, Tag, Answer
3-
from .models.statistics_models import QuestionResponse
2+
from .models.quiz_models import Question, Subject, Topic, Answer
3+
from .models.statistics_models import QuizStatistics
44

55

66
class AnswerInline(admin.TabularInline):
77
model = Answer
88

99

10-
class TagInline(admin.TabularInline):
11-
model = Tag.question.through
12-
verbose_name = "Tag"
10+
class SubjectInline(admin.TabularInline):
11+
model = Topic.question.through
12+
verbose_name = "Subject"
13+
14+
15+
class TopicInline(admin.TabularInline):
16+
model = Subject
17+
verbose_name = "Topic"
1318

1419

1520
@admin.register(Question)
1621
class QuestionAdmin(admin.ModelAdmin):
1722
inlines = [
1823
AnswerInline,
19-
TagInline,
24+
SubjectInline,
2025
]
2126

2227

23-
admin.site.register(Tag)
24-
admin.site.register(QuestionResponse)
28+
admin.site.register(Subject)
29+
admin.site.register(Topic)
30+
admin.site.register(QuizStatistics)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Generated by Django 4.0.6 on 2022-07-23 06:02
2+
3+
import datetime
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
("shared_models", "0004_question_creator_questionresponse_user"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="QuizStatistics",
18+
fields=[
19+
(
20+
"id",
21+
models.BigAutoField(
22+
auto_created=True,
23+
primary_key=True,
24+
serialize=False,
25+
verbose_name="ID",
26+
),
27+
),
28+
("name", models.CharField(max_length=200)),
29+
("question_count", models.PositiveIntegerField()),
30+
("total_marks", models.PositiveIntegerField()),
31+
("user_mark", models.PositiveIntegerField()),
32+
(
33+
"time_taken",
34+
models.DurationField(default=datetime.timedelta(0)),
35+
),
36+
(
37+
"date_taken",
38+
models.DateTimeField(auto_now_add=True, null=True),
39+
),
40+
],
41+
),
42+
migrations.CreateModel(
43+
name="Subject",
44+
fields=[
45+
(
46+
"id",
47+
models.BigAutoField(
48+
auto_created=True,
49+
primary_key=True,
50+
serialize=False,
51+
verbose_name="ID",
52+
),
53+
),
54+
("name", models.CharField(max_length=100)),
55+
],
56+
),
57+
migrations.CreateModel(
58+
name="Topic",
59+
fields=[
60+
(
61+
"id",
62+
models.BigAutoField(
63+
auto_created=True,
64+
primary_key=True,
65+
serialize=False,
66+
verbose_name="ID",
67+
),
68+
),
69+
("name", models.CharField(max_length=100)),
70+
(
71+
"subject",
72+
models.ForeignKey(
73+
on_delete=django.db.models.deletion.CASCADE,
74+
to="shared_models.subject",
75+
),
76+
),
77+
],
78+
),
79+
migrations.RemoveField(
80+
model_name="tag",
81+
name="question",
82+
),
83+
migrations.AddField(
84+
model_name="question",
85+
name="is_verified",
86+
field=models.BooleanField(default=False),
87+
),
88+
migrations.AddField(
89+
model_name="question",
90+
name="mark",
91+
field=models.PositiveIntegerField(null=True),
92+
),
93+
migrations.DeleteModel(
94+
name="QuestionResponse",
95+
),
96+
migrations.DeleteModel(
97+
name="Tag",
98+
),
99+
migrations.AddField(
100+
model_name="subject",
101+
name="question",
102+
field=models.ManyToManyField(to="shared_models.question"),
103+
),
104+
migrations.AddField(
105+
model_name="quizstatistics",
106+
name="subjects",
107+
field=models.ManyToManyField(to="shared_models.subject"),
108+
),
109+
migrations.AddField(
110+
model_name="quizstatistics",
111+
name="topics",
112+
field=models.ManyToManyField(to="shared_models.topic"),
113+
),
114+
migrations.AddField(
115+
model_name="quizstatistics",
116+
name="user",
117+
field=models.ForeignKey(
118+
null=True,
119+
on_delete=django.db.models.deletion.SET_NULL,
120+
to=settings.AUTH_USER_MODEL,
121+
),
122+
),
123+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.0.6 on 2022-07-23 11:02
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
(
9+
"shared_models",
10+
"0005_quizstatistics_subject_topic_remove_tag_question_and_more",
11+
),
12+
]
13+
14+
operations = [
15+
migrations.RemoveField(
16+
model_name="subject",
17+
name="question",
18+
),
19+
migrations.AddField(
20+
model_name="topic",
21+
name="question",
22+
field=models.ManyToManyField(to="shared_models.question"),
23+
),
24+
]

server/api/apps/shared_models/models/quiz_models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,24 @@ class QuestionType(models.TextChoices):
1919
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
2020
)
2121
date_created = models.DateTimeField(auto_now_add=True, null=True)
22+
is_verified = models.BooleanField(default=False)
23+
mark = models.PositiveIntegerField(null=True)
2224

2325
def __str__(self):
2426
return self.text
2527

2628

27-
class Tag(models.Model):
29+
class Subject(models.Model):
30+
name = models.CharField(max_length=defines.TAG_NAME_MAXLEN)
31+
32+
def __str__(self):
33+
return self.name
34+
35+
36+
class Topic(models.Model):
2837
name = models.CharField(max_length=defines.TAG_NAME_MAXLEN)
2938
question = models.ManyToManyField(Question)
39+
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
3040

3141
def __str__(self):
3242
return self.name
Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
from django.conf import settings
22
from django.db import models
3-
from .quiz_models import Question, Answer
4-
3+
from .quiz_models import Subject, Topic
4+
from . import defines
55
from datetime import timedelta
66

77

8-
class QuestionResponse(models.Model):
8+
class QuizStatistics(models.Model):
99
user = models.ForeignKey(
1010
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True
1111
)
12-
question = models.ForeignKey(Question, on_delete=models.CASCADE)
13-
selected_answer = models.ForeignKey(
14-
Answer, on_delete=models.CASCADE, null=True
15-
)
16-
is_correct = models.BooleanField(default=False)
12+
name = models.CharField(max_length=defines.QUIZ_NAME_MAXLEN)
13+
question_count = models.PositiveIntegerField()
14+
total_marks = models.PositiveIntegerField()
15+
user_mark = models.PositiveIntegerField()
16+
subjects = models.ManyToManyField(Subject)
17+
topics = models.ManyToManyField(Topic)
1718
time_taken = models.DurationField(default=timedelta())
18-
date_created = models.DateTimeField(auto_now_add=True, null=True)
19+
date_taken = models.DateTimeField(auto_now_add=True, null=True)
1920

2021
def __str__(self):
21-
return str((self.question, self.selected_answer))
22+
return str(self.name)

server/api/apps/shared_models/serializers/quiz_serializers.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@
55
class QuestionSerializer(serializers.HyperlinkedModelSerializer):
66
class Meta:
77
model = Question
8-
fields = ["id", "text", "question_type", "date_created"]
8+
fields = ["id", "text", "question_type", "date_created", "mark"]
99

1010

11-
class TagSerializer(serializers.HyperlinkedModelSerializer):
11+
class SubjectSerializer(serializers.HyperlinkedModelSerializer):
1212
class Meta:
1313
model = Tag
14-
fields = ["id", "name", "question"]
14+
fields = ["id", "name"]
15+
16+
17+
class TopicSerializer(serializers.HyperlinkedModelSerializer):
18+
class Meta:
19+
model = Tag
20+
fields = ["id", "name"]
1521

1622

1723
class AnswerSerializer(serializers.HyperlinkedModelSerializer):
1824
class Meta:
1925
model = Answer
20-
fields = ["id", "text", "is_correct", "question"]
26+
fields = ["id", "text", "is_correct"]

server/api/apps/shared_models/serializers/statistics_serializers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ class Meta:
77
model = QuestionResponse
88
fields = [
99
"id",
10-
"question",
11-
"selected_answer",
12-
"is_correct",
10+
"question_count",
11+
"total_marks",
12+
"user_mark",
1313
"time_taken",
14-
"date_created",
14+
"date_taken",
1515
]

server/api/apps/shared_models/tests/test_quiz.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django.test import TestCase
22
from django.utils import timezone
3-
from ..models.quiz_models import Question, Tag, Answer
3+
from ..models.quiz_models import Question, Answer, Subject, Topic
44

55
MC = Question.QuestionType.MULTICHOICE
66
NA = Question.QuestionType.NUMERIC
@@ -58,15 +58,16 @@ def setUp(self):
5858
q = Question.objects.create(
5959
text="This is a physics question", question_type=NA
6060
)
61-
t = Tag.objects.create(name="Unit 3 Physics")
62-
q.tag_set.add(t)
61+
s = Subject.objects.create(name="Physics")
62+
t = Topic.objects.create(name="Unit 3 Physics", subject=s)
63+
q.topic_set.add(t)
6364

6465
def test(self):
6566
q = Question.objects.get(text="This is a physics question")
66-
t = Tag.objects.get(name="Unit 3 Physics")
67+
t = Topic.objects.get(name="Unit 3 Physics")
6768
self.assertEqual(str(t), "Unit 3 Physics")
6869

69-
self.assertTrue(t in q.tag_set.all())
70+
self.assertTrue(t in q.topic_set.all())
7071

7172

7273
class AnswerTestCase(TestCase):
Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.test import TestCase
22
from django.utils import timezone
3-
from ..models.quiz_models import Question, Answer
4-
from ..models.statistics_models import QuestionResponse
3+
from ..models.quiz_models import Question
4+
from ..models.statistics_models import QuizStatistics
55

66
from datetime import timedelta
77

@@ -11,27 +11,23 @@
1111
class QuestionTestCase(TestCase):
1212
def setUp(self):
1313
self.creation_time = timezone.now()
14-
q = Question.objects.create(text="Question?", question_type=MC)
15-
a = Answer.objects.create(text="Answer!", question=q, is_correct=True)
16-
QuestionResponse.objects.create(
17-
question=q,
18-
selected_answer=a,
19-
is_correct=a.is_correct,
14+
QuizStatistics.objects.create(
15+
name="name",
16+
question_count=3,
17+
total_marks=3,
18+
user_mark=2,
2019
time_taken=timedelta(seconds=5),
2120
)
2221

2322
def test(self):
24-
q = Question.objects.get(text="Question?")
25-
a = Answer.objects.get(text="Answer!")
26-
qr = QuestionResponse.objects.all()[0]
23+
qr = QuizStatistics.objects.all()[0]
2724

28-
self.assertEquals(str(qr), str((q, a)))
2925
self.assertIsNone(qr.user)
30-
self.assertEquals(qr.question, q)
31-
self.assertEquals(qr.selected_answer, a)
32-
self.assertEquals(qr.is_correct, a.is_correct)
33-
self.assertEquals(qr.is_correct, True)
26+
self.assertEquals(qr.name, "name")
27+
self.assertEquals(qr.question_count, 3)
28+
self.assertEquals(qr.total_marks, 3)
29+
self.assertEquals(qr.user_mark, 2)
3430
self.assertEquals(qr.time_taken, timedelta(seconds=5))
3531
self.assertLess(
36-
(qr.date_created - self.creation_time).total_seconds(), 0.1
32+
(qr.date_taken - self.creation_time).total_seconds(), 0.1
3733
)

0 commit comments

Comments
 (0)