Skip to content

Commit 7329152

Browse files
question CRUD
1 parent adc250d commit 7329152

File tree

13 files changed

+169
-4
lines changed

13 files changed

+169
-4
lines changed

classroom/filters/teacher/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .classroom import ClassroomTeacherFilter
22
from .exercise import ReadingExerciseTeacherFilter
3+
from .question import ReadingQuestionTeacherFilter

classroom/filters/teacher/question.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django_filters import rest_framework as filters
2+
3+
from classroom.models import ReadingQuestion
4+
5+
6+
class ReadingQuestionTeacherFilter(filters.FilterSet):
7+
class Meta:
8+
model = ReadingQuestion
9+
fields = {
10+
'exercise': ['exact'],
11+
}
12+
13+
@property
14+
def qs(self):
15+
parent = super().qs
16+
user = self.request.user
17+
exercises = user.reading_exercises_created.all()
18+
questions_pks = ReadingQuestion.objects.filter(exercise__in=exercises).values_list('pk', flat=True)
19+
qs = parent.filter(pk__in=list(questions_pks))\
20+
.select_related('exercise__creator')
21+
return qs
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 3.2.7 on 2021-10-04 16:04
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('classroom', '0004_alter_readingexercise_unique_together'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='readinganswer',
15+
name='letter',
16+
field=models.CharField(blank=True, max_length=10),
17+
),
18+
migrations.AlterField(
19+
model_name='readingquestion',
20+
name='question_type',
21+
field=models.CharField(choices=[('multiple_choice', 'Multiple choice'), ('true_false', 'True / False / Not given'), ('yes_no', 'Yes / No / Not given'), ('fill_blank', 'Fill in the blank')], max_length=30),
22+
),
23+
migrations.AlterField(
24+
model_name='readingquestion',
25+
name='to_number',
26+
field=models.PositiveSmallIntegerField(null=True),
27+
),
28+
]

classroom/models/abstracts/answer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
class Answer(models.Model):
5-
letter = models.CharField(max_length=1, blank=True)
5+
letter = models.CharField(max_length=10, blank=True)
66
content = models.TextField()
77

88
class Meta:

classroom/models/abstracts/question.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,23 @@
55
class Question(models.Model):
66
class Types(models.TextChoices):
77
MULTIPLE_CHOICE = 'multiple_choice', _('Multiple choice')
8-
TRUE_FALSE = 'true_false', _('True/False/Not given')
9-
YES_NO = 'yes_no', _('Yes/No/Not given')
8+
TRUE_FALSE = 'true_false', _('True / False / Not given')
9+
YES_NO = 'yes_no', _('Yes / No / Not given')
1010
FILL_BLANK = 'fill_blank', _('Fill in the blank')
1111

1212
from_number = models.PositiveSmallIntegerField()
13-
to_number = models.PositiveSmallIntegerField()
13+
to_number = models.PositiveSmallIntegerField(null=True)
1414
question_type = models.CharField(max_length=30, choices=Types.choices)
1515
correct_answer = models.CharField(max_length=255)
1616

1717
class Meta:
1818
abstract = True
19+
20+
def __str__(self):
21+
return f'{self.get_question_number_display()} | {self.question_type} | {self.correct_answer[:50]}'
22+
23+
def get_question_number_display(self):
24+
display = str(self.from_number)
25+
if self.to_number is not None and self.to_number != self.from_number:
26+
display += f' - {self.to_number}'
27+
return display

classroom/models/reading_answer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ class ReadingAnswer(Answer):
99

1010
class Meta:
1111
ordering = ['question', 'letter', 'pk']
12+
13+
def __str__(self):
14+
return f'{self.letter}. {self.content[:50]}'

classroom/models/reading_exercise.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,21 @@ class ReadingExercise(models.Model):
1515
class Meta:
1616
ordering = ['identifier']
1717
unique_together = ['creator', 'identifier']
18+
19+
def __str__(self):
20+
display = self.identifier
21+
if self.creator:
22+
display += f' | {self.creator.email}'
23+
return display
24+
25+
def get_question_range(self):
26+
question_numbers = self.questions.order_by('from_number').values('from_number', 'to_number')
27+
question_range = []
28+
for number in question_numbers:
29+
from_number = number['from_number']
30+
to_number = number['to_number']
31+
if to_number is None:
32+
question_range.append(from_number)
33+
else:
34+
question_range.extend(range(from_number, to_number + 1))
35+
return question_range

classroom/permissions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ def has_object_permission(self, request, view, exercise):
3232
)
3333

3434

35+
class IsTeacherOwnsQuestion(IsTeacher):
36+
message = _('Only teacher who owns this question has permission for this.')
37+
38+
def has_object_permission(self, request, view, question):
39+
return (
40+
super().has_object_permission(request, view, question) and
41+
request.user == question.exercise.creator
42+
)
43+
44+
3545
class IsStudent(IsAuthenticated):
3646
message = _('Only student has permission for this.')
3747

classroom/serializers/teacher/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
RemoveReadingExerciseSerializer,
44
RemoveStudentSerializer)
55
from .exercise import ReadingExerciseSerializer
6+
from .question import ReadingQuestionSerializer
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from django.utils.translation import gettext as _
2+
from rest_framework import serializers
3+
4+
from classroom.models import ReadingQuestion
5+
6+
7+
class ReadingQuestionSerializer(serializers.HyperlinkedModelSerializer):
8+
class Meta:
9+
model = ReadingQuestion
10+
fields = [
11+
'pk', 'url', 'exercise',
12+
'from_number', 'to_number', 'question_type', 'correct_answer',
13+
]
14+
extra_kwargs = {
15+
'url': {'view_name': 'reading-question-teacher-detail'},
16+
'exercise': {'view_name': 'reading-exercise-teacher-detail'},
17+
'from_number': {'min_value': 1},
18+
'to_number': {'min_value': 1},
19+
}
20+
21+
def validate(self, attrs):
22+
attrs = super().validate(attrs)
23+
self._validate_from_to_number(attrs)
24+
self._validate_question_not_exist(attrs)
25+
return attrs
26+
27+
def _validate_from_to_number(self, attrs):
28+
from_number = attrs['from_number']
29+
to_number = attrs['to_number']
30+
if to_number is not None and not to_number > from_number:
31+
raise serializers.ValidationError(
32+
_('"To" must be greater than "From".')
33+
)
34+
35+
def _validate_question_not_exist(self, attrs):
36+
from_number = attrs['from_number']
37+
exercise = attrs['exercise']
38+
question_range = exercise.get_question_range()
39+
40+
if self.instance:
41+
if self.instance.to_number is not None:
42+
instance_range = range(self.instance.from_number, self.instance.to_number + 1)
43+
else:
44+
instance_range = [self.instance.from_number]
45+
for number in instance_range:
46+
try:
47+
index = question_range.index(number)
48+
question_range.pop(index)
49+
except ValueError:
50+
pass
51+
52+
if from_number in question_range:
53+
raise serializers.ValidationError(
54+
_('This question number already exists.')
55+
)
56+
57+
# TODO: validation regarding `question_type` and `correct_answer`

0 commit comments

Comments
 (0)