Skip to content

Commit

Permalink
Migrate fields/functionality from Quiz to Assignment (#32)
Browse files Browse the repository at this point in the history
Also rename LogMessage to QuizLogMessage
  • Loading branch information
krishnans2006 authored May 27, 2024
1 parent 3725a59 commit 9c7f97f
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 88 deletions.
26 changes: 13 additions & 13 deletions tin/apps/assignments/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@

from django.contrib import admin

from .models import Assignment, CooldownPeriod, FileAction, Folder, LogMessage, MossResult, Quiz
from .models import (
Assignment,
CooldownPeriod,
FileAction,
Folder,
MossResult,
Quiz,
QuizLogMessage,
)


@admin.register(Folder)
Expand Down Expand Up @@ -86,19 +94,15 @@ def visible(self, obj):
return not obj.assignment.hidden


@admin.register(LogMessage)
class LogMessageAdmin(admin.ModelAdmin):
@admin.register(QuizLogMessage)
class QuizLogMessageAdmin(admin.ModelAdmin):
date_hierarchy = "date"
list_display = ("content", "assignment", "student", "date", "severity")
list_filter = ("student", "severity")
ordering = ("-date",)
save_as = True
search_fields = ("quiz__assignment__name", "student__username", "content")
autocomplete_fields = ("quiz", "student")

@admin.display(description="Assignment")
def assignment(self, obj):
return obj.quiz.assignment.name
search_fields = ("assignment__name", "student__username", "content")
autocomplete_fields = ("assignment", "student")


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

@admin.display(description="Assignment")
def assignment(self, obj):
return obj.quiz.assignment.name

@admin.display(description="Course")
def course_name(self, obj):
return obj.assignment.course.name
Expand Down
22 changes: 15 additions & 7 deletions tin/apps/assignments/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@


class AssignmentForm(forms.ModelForm):
QUIZ_ACTIONS = (("-1", "No"), ("0", "Log only"), ("1", "Color Change"), ("2", "Lock"))

due = forms.DateTimeInput()
is_quiz = forms.ChoiceField(choices=QUIZ_ACTIONS)

def __init__(self, course, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -70,6 +67,8 @@ class Meta:
"submission_limit_count",
"submission_limit_interval",
"submission_limit_cooldown",
"is_quiz",
"quiz_action",
]
labels = {
"markdown": "Use markdown?",
Expand All @@ -93,7 +92,6 @@ class Meta:
"markdown",
"due",
"points_possible",
"is_quiz",
"hidden",
),
},
Expand All @@ -109,7 +107,16 @@ class Meta:
"collapsed": False,
},
{
"name": "Submissions",
"name": "Quiz Options",
"description": "",
"fields": (
"is_quiz",
"quiz_action",
),
"collapsed": False,
},
{
"name": "Other Settings",
"description": "",
"fields": (
"enable_grader_timeout",
Expand Down Expand Up @@ -142,8 +149,9 @@ class Meta:
"submission_limit_cooldown": 'This sets the length of the "cooldown" period after a '
"student exceeds the rate limit for submissions.",
"folder": "If blank, assignment will show on the main classroom page.",
"is_quiz": "If set, Tin will take the selected action if a student clicks off of the "
"submission page.",
"is_quiz": "This forces students to submit through a page that monitors their actions.",
"quiz_action": "Tin will take the selected action if a student clicks off of the "
"quiz page.",
}
widgets = {"description": forms.Textarea(attrs={"cols": 30, "rows": 4})}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Generated by Django 4.2.13 on 2024-05-27 04:12

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("assignments", "0029_assignment_markdown"),
]

operations = [
migrations.CreateModel(
name="QuizLogMessage",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("date", models.DateTimeField(auto_now_add=True)),
("content", models.CharField(max_length=100)),
("severity", models.IntegerField()),
],
),
migrations.AddField(
model_name="assignment",
name="is_quiz",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="assignment",
name="quiz_action",
field=models.CharField(
choices=[("0", "Log only"), ("1", "Color Change"), ("2", "Lock")],
default="2",
max_length=1,
),
),
migrations.DeleteModel(
name="LogMessage",
),
migrations.AddField(
model_name="quizlogmessage",
name="assignment",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="log_messages",
to="assignments.assignment",
),
),
migrations.AddField(
model_name="quizlogmessage",
name="student",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="log_messages",
to=settings.AUTH_USER_MODEL,
),
),
]
47 changes: 33 additions & 14 deletions tin/apps/assignments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ class Assignment(models.Model):

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

is_quiz = models.BooleanField(default=False)
QUIZ_ACTIONS = (("0", "Log only"), ("1", "Color Change"), ("2", "Lock"))
quiz_action = models.CharField(max_length=1, choices=QUIZ_ACTIONS, default="2")

objects = AssignmentQuerySet.as_manager()

def __str__(self):
Expand Down Expand Up @@ -279,11 +283,23 @@ def grader_log_filename(self):
else None
)

@property
def is_quiz(self):
if hasattr(self, "quiz"):
return self.quiz
return False
def quiz_open_for_student(self, student):
is_teacher = self.course.teacher.filter(id=student.id).exists()
if is_teacher or student.is_superuser:
return True
return not (self.quiz_ended_for_student(student) or self.quiz_locked_for_student(student))

def quiz_ended_for_student(self, student):
return self.log_messages.filter(student=student, content="Ended quiz").exists()

def quiz_locked_for_student(self, student):
return self.quiz_issues_for_student(student) and self.quiz_action == "2"

def quiz_issues_for_student(self, student):
return (
sum(lm.severity for lm in self.log_messages.filter(student=student))
>= settings.QUIZ_ISSUE_THRESHOLD
)


class CooldownPeriod(models.Model):
Expand Down Expand Up @@ -335,6 +351,9 @@ def get_time_to_end(self) -> datetime.timedelta:
)


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

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

def issues_for_student(self, student):
return (
sum(lm.severity for lm in self.log_messages.filter(student=student))
sum(lm.severity for lm in self.assignment.log_messages.filter(student=student))
>= settings.QUIZ_ISSUE_THRESHOLD
)

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

def ended_for_student(self, student):
return self.log_messages.filter(student=student, content="Ended quiz").exists()
return self.assignment.log_messages.filter(student=student, content="Ended quiz").exists()


class LogMessage(models.Model):
quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name="log_messages")
class QuizLogMessage(models.Model):
assignment = models.ForeignKey(
Assignment, on_delete=models.CASCADE, related_name="log_messages"
)
student = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="log_messages"
)
Expand All @@ -386,15 +407,13 @@ class LogMessage(models.Model):
severity = models.IntegerField()

def __str__(self):
return f"{self.content} for {self.quiz}"
return f"{self.content} for {self.assignment} by {self.student}"

def get_absolute_url(self):
return reverse(
"assignments:student_submission", args=(self.quiz.assignment.id, self.student.id)
)
return reverse("assignments:student_submission", args=(self.assignment.id, self.student.id))

def __repr__(self):
return f"{self.content} for {self.quiz}"
return f"{self.content} for {self.assignment} by {self.student}"


def moss_base_file_path(obj, _): # pylint: disable=unused-argument
Expand Down
12 changes: 3 additions & 9 deletions tin/apps/assignments/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import pytest
from django.urls import reverse

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


@teacher
@pytest.mark.parametrize("is_quiz", (-1, 0, 1, 2))
def test_create_assignment(client, course, is_quiz) -> None:
def test_create_assignment(client, course) -> None:
data = {
"name": "Write a Vertex Shader",
"description": "See https://learnopengl.com/Getting-started/Shaders",
"language": "P",
"is_quiz": is_quiz,
"filename": "vertex.glsl",
"points_possible": "300",
"due": "04/16/2025",
"grader_timeout": "300",
"submission_limit_count": "90",
"submission_limit_interval": "30",
"submission_limit_cooldown": "30",
"is_quiz": False,
"quiz_action": "2",
}
response = client.post(
reverse("assignments:add", args=[course.id]),
Expand All @@ -38,8 +37,3 @@ def test_create_assignment(client, course, is_quiz) -> None:
assert is_redirect(response)
assignment_set = course.assignments.filter(name__exact=data["name"])
assert assignment_set.count() == 1
assignment = assignment_set.get()
if is_quiz != -1:
assert assignment.quiz.action == str(is_quiz)
else:
assert not hasattr(assignment, "quiz")
Loading

0 comments on commit 9c7f97f

Please sign in to comment.