Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate fields/functionality from Quiz to Assignment #32

Merged
merged 3 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = student in self.course.teacher.all()
krishnans2006 marked this conversation as resolved.
Show resolved Hide resolved
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))
krishnans2006 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading