Skip to content

Commit 9c7f97f

Browse files
Migrate fields/functionality from Quiz to Assignment (#32)
Also rename LogMessage to QuizLogMessage
1 parent 3725a59 commit 9c7f97f

7 files changed

Lines changed: 149 additions & 88 deletions

File tree

tin/apps/assignments/admin.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44

55
from django.contrib import admin
66

7-
from .models import Assignment, CooldownPeriod, FileAction, Folder, LogMessage, MossResult, Quiz
7+
from .models import (
8+
Assignment,
9+
CooldownPeriod,
10+
FileAction,
11+
Folder,
12+
MossResult,
13+
Quiz,
14+
QuizLogMessage,
15+
)
816

917

1018
@admin.register(Folder)
@@ -86,19 +94,15 @@ def visible(self, obj):
8694
return not obj.assignment.hidden
8795

8896

89-
@admin.register(LogMessage)
90-
class LogMessageAdmin(admin.ModelAdmin):
97+
@admin.register(QuizLogMessage)
98+
class QuizLogMessageAdmin(admin.ModelAdmin):
9199
date_hierarchy = "date"
92100
list_display = ("content", "assignment", "student", "date", "severity")
93101
list_filter = ("student", "severity")
94102
ordering = ("-date",)
95103
save_as = True
96-
search_fields = ("quiz__assignment__name", "student__username", "content")
97-
autocomplete_fields = ("quiz", "student")
98-
99-
@admin.display(description="Assignment")
100-
def assignment(self, obj):
101-
return obj.quiz.assignment.name
104+
search_fields = ("assignment__name", "student__username", "content")
105+
autocomplete_fields = ("assignment", "student")
102106

103107

104108
@admin.register(MossResult)
@@ -111,10 +115,6 @@ class MossResultAdmin(admin.ModelAdmin):
111115
search_fields = ("assignment__name", "url")
112116
autocomplete_fields = ("assignment",)
113117

114-
@admin.display(description="Assignment")
115-
def assignment(self, obj):
116-
return obj.quiz.assignment.name
117-
118118
@admin.display(description="Course")
119119
def course_name(self, obj):
120120
return obj.assignment.course.name

tin/apps/assignments/forms.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@
1313

1414

1515
class AssignmentForm(forms.ModelForm):
16-
QUIZ_ACTIONS = (("-1", "No"), ("0", "Log only"), ("1", "Color Change"), ("2", "Lock"))
17-
1816
due = forms.DateTimeInput()
19-
is_quiz = forms.ChoiceField(choices=QUIZ_ACTIONS)
2017

2118
def __init__(self, course, *args, **kwargs):
2219
super().__init__(*args, **kwargs)
@@ -70,6 +67,8 @@ class Meta:
7067
"submission_limit_count",
7168
"submission_limit_interval",
7269
"submission_limit_cooldown",
70+
"is_quiz",
71+
"quiz_action",
7372
]
7473
labels = {
7574
"markdown": "Use markdown?",
@@ -93,7 +92,6 @@ class Meta:
9392
"markdown",
9493
"due",
9594
"points_possible",
96-
"is_quiz",
9795
"hidden",
9896
),
9997
},
@@ -109,7 +107,16 @@ class Meta:
109107
"collapsed": False,
110108
},
111109
{
112-
"name": "Submissions",
110+
"name": "Quiz Options",
111+
"description": "",
112+
"fields": (
113+
"is_quiz",
114+
"quiz_action",
115+
),
116+
"collapsed": False,
117+
},
118+
{
119+
"name": "Other Settings",
113120
"description": "",
114121
"fields": (
115122
"enable_grader_timeout",
@@ -142,8 +149,9 @@ class Meta:
142149
"submission_limit_cooldown": 'This sets the length of the "cooldown" period after a '
143150
"student exceeds the rate limit for submissions.",
144151
"folder": "If blank, assignment will show on the main classroom page.",
145-
"is_quiz": "If set, Tin will take the selected action if a student clicks off of the "
146-
"submission page.",
152+
"is_quiz": "This forces students to submit through a page that monitors their actions.",
153+
"quiz_action": "Tin will take the selected action if a student clicks off of the "
154+
"quiz page.",
147155
}
148156
widgets = {"description": forms.Textarea(attrs={"cols": 30, "rows": 4})}
149157

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Generated by Django 4.2.13 on 2024-05-27 04:12
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
("assignments", "0029_assignment_markdown"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="QuizLogMessage",
18+
fields=[
19+
(
20+
"id",
21+
models.BigAutoField(
22+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
23+
),
24+
),
25+
("date", models.DateTimeField(auto_now_add=True)),
26+
("content", models.CharField(max_length=100)),
27+
("severity", models.IntegerField()),
28+
],
29+
),
30+
migrations.AddField(
31+
model_name="assignment",
32+
name="is_quiz",
33+
field=models.BooleanField(default=False),
34+
),
35+
migrations.AddField(
36+
model_name="assignment",
37+
name="quiz_action",
38+
field=models.CharField(
39+
choices=[("0", "Log only"), ("1", "Color Change"), ("2", "Lock")],
40+
default="2",
41+
max_length=1,
42+
),
43+
),
44+
migrations.DeleteModel(
45+
name="LogMessage",
46+
),
47+
migrations.AddField(
48+
model_name="quizlogmessage",
49+
name="assignment",
50+
field=models.ForeignKey(
51+
on_delete=django.db.models.deletion.CASCADE,
52+
related_name="log_messages",
53+
to="assignments.assignment",
54+
),
55+
),
56+
migrations.AddField(
57+
model_name="quizlogmessage",
58+
name="student",
59+
field=models.ForeignKey(
60+
on_delete=django.db.models.deletion.CASCADE,
61+
related_name="log_messages",
62+
to=settings.AUTH_USER_MODEL,
63+
),
64+
),
65+
]

tin/apps/assignments/models.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ class Assignment(models.Model):
125125

126126
last_action_output = models.CharField(max_length=16 * 1024, default="", null=False, blank=True)
127127

128+
is_quiz = models.BooleanField(default=False)
129+
QUIZ_ACTIONS = (("0", "Log only"), ("1", "Color Change"), ("2", "Lock"))
130+
quiz_action = models.CharField(max_length=1, choices=QUIZ_ACTIONS, default="2")
131+
128132
objects = AssignmentQuerySet.as_manager()
129133

130134
def __str__(self):
@@ -279,11 +283,23 @@ def grader_log_filename(self):
279283
else None
280284
)
281285

282-
@property
283-
def is_quiz(self):
284-
if hasattr(self, "quiz"):
285-
return self.quiz
286-
return False
286+
def quiz_open_for_student(self, student):
287+
is_teacher = self.course.teacher.filter(id=student.id).exists()
288+
if is_teacher or student.is_superuser:
289+
return True
290+
return not (self.quiz_ended_for_student(student) or self.quiz_locked_for_student(student))
291+
292+
def quiz_ended_for_student(self, student):
293+
return self.log_messages.filter(student=student, content="Ended quiz").exists()
294+
295+
def quiz_locked_for_student(self, student):
296+
return self.quiz_issues_for_student(student) and self.quiz_action == "2"
297+
298+
def quiz_issues_for_student(self, student):
299+
return (
300+
sum(lm.severity for lm in self.log_messages.filter(student=student))
301+
>= settings.QUIZ_ISSUE_THRESHOLD
302+
)
287303

288304

289305
class CooldownPeriod(models.Model):
@@ -335,6 +351,9 @@ def get_time_to_end(self) -> datetime.timedelta:
335351
)
336352

337353

354+
# WARNING: This model is deprecated and will be removed in the future.
355+
# It is kept for backwards compatibility with existing data.
356+
# All fields and methods have been migrated to the Assignment model.
338357
class Quiz(models.Model):
339358
QUIZ_ACTIONS = (("0", "Log only"), ("1", "Color Change"), ("2", "Lock"))
340359

@@ -358,7 +377,7 @@ def __repr__(self):
358377

359378
def issues_for_student(self, student):
360379
return (
361-
sum(lm.severity for lm in self.log_messages.filter(student=student))
380+
sum(lm.severity for lm in self.assignment.log_messages.filter(student=student))
362381
>= settings.QUIZ_ISSUE_THRESHOLD
363382
)
364383

@@ -372,11 +391,13 @@ def locked_for_student(self, student):
372391
return self.issues_for_student(student) and self.action == "2"
373392

374393
def ended_for_student(self, student):
375-
return self.log_messages.filter(student=student, content="Ended quiz").exists()
394+
return self.assignment.log_messages.filter(student=student, content="Ended quiz").exists()
376395

377396

378-
class LogMessage(models.Model):
379-
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name="log_messages")
397+
class QuizLogMessage(models.Model):
398+
assignment = models.ForeignKey(
399+
Assignment, on_delete=models.CASCADE, related_name="log_messages"
400+
)
380401
student = models.ForeignKey(
381402
get_user_model(), on_delete=models.CASCADE, related_name="log_messages"
382403
)
@@ -386,15 +407,13 @@ class LogMessage(models.Model):
386407
severity = models.IntegerField()
387408

388409
def __str__(self):
389-
return f"{self.content} for {self.quiz}"
410+
return f"{self.content} for {self.assignment} by {self.student}"
390411

391412
def get_absolute_url(self):
392-
return reverse(
393-
"assignments:student_submission", args=(self.quiz.assignment.id, self.student.id)
394-
)
413+
return reverse("assignments:student_submission", args=(self.assignment.id, self.student.id))
395414

396415
def __repr__(self):
397-
return f"{self.content} for {self.quiz}"
416+
return f"{self.content} for {self.assignment} by {self.student}"
398417

399418

400419
def moss_base_file_path(obj, _): # pylint: disable=unused-argument

tin/apps/assignments/tests.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import pytest
43
from django.urls import reverse
54

65
from tin.tests import is_redirect, teacher
@@ -16,20 +15,20 @@ def test_create_folder(client, course) -> None:
1615

1716

1817
@teacher
19-
@pytest.mark.parametrize("is_quiz", (-1, 0, 1, 2))
20-
def test_create_assignment(client, course, is_quiz) -> None:
18+
def test_create_assignment(client, course) -> None:
2119
data = {
2220
"name": "Write a Vertex Shader",
2321
"description": "See https://learnopengl.com/Getting-started/Shaders",
2422
"language": "P",
25-
"is_quiz": is_quiz,
2623
"filename": "vertex.glsl",
2724
"points_possible": "300",
2825
"due": "04/16/2025",
2926
"grader_timeout": "300",
3027
"submission_limit_count": "90",
3128
"submission_limit_interval": "30",
3229
"submission_limit_cooldown": "30",
30+
"is_quiz": False,
31+
"quiz_action": "2",
3332
}
3433
response = client.post(
3534
reverse("assignments:add", args=[course.id]),
@@ -38,8 +37,3 @@ def test_create_assignment(client, course, is_quiz) -> None:
3837
assert is_redirect(response)
3938
assignment_set = course.assignments.filter(name__exact=data["name"])
4039
assert assignment_set.count() == 1
41-
assignment = assignment_set.get()
42-
if is_quiz != -1:
43-
assert assignment.quiz.action == str(is_quiz)
44-
else:
45-
assert not hasattr(assignment, "quiz")

0 commit comments

Comments
 (0)