Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 6 additions & 3 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ concurrency:

jobs:
quality:
env:
SECRET_KEY: ci-placeholder
DB_PASSWORD: ci-placeholder
# A job that calls a reusable workflow (`uses:`) may not declare `env:` —
# GitHub rejects the whole workflow file as invalid ("workflow file issue"),
# which is why every run of this workflow has failed since 6a6fed1f
# (2026-10-27). The block was also inert: reusable-workflow jobs never see
# the caller's job env. SECRET_KEY/DB_PASSWORD for the settings contract
# are set where they belong, in this repo's own verify job.
# Pinned to the v1 tag's commit hash (b4fbe38) — CodeQL red-blocker
# (actions/unpinned-tag) rejects mutable tag refs; this is v1, immutably.
uses: Prekzursil/quality-zero-platform/.github/workflows/reusable-quality.yml@b4fbe38470fd2831f86f194df5b638a271bae8b8 # v1
Expand Down
10 changes: 9 additions & 1 deletion backend/classroom/admin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.contrib import admin

Check warning on line 1 in backend/classroom/admin.py

View workflow job for this annotation

GitHub Actions / quality / quality

Import "django.contrib" could not be resolved (reportMissingImports)

from .models import (
Assignment,
Expand Down Expand Up @@ -32,11 +32,19 @@

@admin.register(Assignment)
class AssignmentAdmin(admin.ModelAdmin):
list_display = ("__str__", "classroom", "available_from", "due_at", "late_until", "is_exam")
list_display = (
"__str__",
"classroom",
"available_from",
"due_at",
"late_until",
"is_exam",
)
search_fields = ("classroom__name",)
list_filter = ("is_exam",)
inlines = [AssignmentProblemInline]


@admin.register(MCQItem)
class MCQItemAdmin(admin.ModelAdmin):
list_display = ("id", "grade", "discipline", "bac_year", "chapter", "author")
Expand Down
18 changes: 11 additions & 7 deletions backend/classroom/exam.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import re

from django.utils.translation import gettext as _

Check warning on line 12 in backend/classroom/exam.py

View workflow job for this annotation

GitHub Actions / quality / quality

Import "django.utils.translation" could not be resolved (reportMissingImports)

#: API prefixes that stay reachable during an exam (account + classroom +
#: own submissions + notifications). Everything else under /api/ closes.
Expand Down Expand Up @@ -37,14 +37,19 @@
"""
if user is None or not getattr(user, "is_authenticated", False):
return None
if user.is_staff or user.is_superuser or getattr(user, "role", "") in (
"TEACHER",
"ADMIN",
if (
user.is_staff
or user.is_superuser
or getattr(user, "role", "")
in (
"TEACHER",
"ADMIN",
)
):
return None
from django.utils import timezone

Check warning on line 50 in backend/classroom/exam.py

View workflow job for this annotation

GitHub Actions / quality / quality

Import "django.utils" could not be resolved (reportMissingImports)

from .models import Assignment, ClassMembership
from .models import Assignment

now = timezone.now()
return (
Expand All @@ -61,9 +66,9 @@

def exam_problem_ids(user) -> set[int]:
"""Problem ids visible to the sitting student (union over open exams)."""
from django.utils import timezone

Check warning on line 69 in backend/classroom/exam.py

View workflow job for this annotation

GitHub Actions / quality / quality

Import "django.utils" could not be resolved (reportMissingImports)

from .models import Assignment, ClassMembership
from .models import Assignment

now = timezone.now()
return set(
Expand All @@ -72,8 +77,7 @@
is_exam=True,
available_from__lte=now,
due_at__gt=now,
)
.values_list("assignment_problems__problem_id", flat=True)
).values_list("assignment_problems__problem_id", flat=True)
) - {None}


Expand Down Expand Up @@ -150,7 +154,7 @@
if reason is not None:
import json

from django.http import HttpResponse

Check warning on line 157 in backend/classroom/exam.py

View workflow job for this annotation

GitHub Actions / quality / quality

Import "django.http" could not be resolved (reportMissingImports)

payload = json.dumps(
{
Expand Down
4 changes: 1 addition & 3 deletions backend/classroom/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@ def grid_csv(assignment) -> str:
buffer = io.StringIO()
writer = csv.writer(buffer)
writer.writerow(
["username"]
+ [f"{ap.label} ({ap.points}p)" for ap in problems]
+ ["total"]
["username"] + [f"{ap.label} ({ap.points}p)" for ap in problems] + ["total"]
)
for student in students:
row = [student.username]
Expand Down
7 changes: 3 additions & 4 deletions backend/classroom/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ def assignment_grid(assignment) -> dict:
)
)
problems = list(
assignment.assignment_problems.select_related("problem").order_by(
"order", "id"
)
assignment.assignment_problems.select_related("problem").order_by("order", "id")
)

students = [m.student for m in memberships]
Expand All @@ -57,7 +55,8 @@ def assignment_grid(assignment) -> dict:
cells: dict[tuple[int, int], dict] = {}
for key, subs in grouped.items():
best = max(
subs, key=lambda s: ((s.score if s.score is not None else 0), s.submission_time)
subs,
key=lambda s: ((s.score if s.score is not None else 0), s.submission_time),
)
best_score = best.score if best.score is not None else 0
cells[key] = {
Expand Down
250 changes: 202 additions & 48 deletions backend/classroom/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,83 +6,237 @@


class Migration(migrations.Migration):

initial = True

dependencies = [
('problems', '0010_testcase_is_pretest'),
("problems", "0010_testcase_is_pretest"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='ClassRoom',
name="ClassRoom",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=120, verbose_name='name')),
('school', models.CharField(blank=True, max_length=160, verbose_name='school')),
('join_code', models.CharField(editable=False, help_text='Shareable code students use to self-join.', max_length=8, unique=True, verbose_name='join code')),
('join_password', models.CharField(blank=True, default='', help_text='Optional second factor for joining; empty = code only.', max_length=128, verbose_name='join password (hashed)')),
('is_open', models.BooleanField(default=True, help_text='Closed classes reject new self-joins; members stay.', verbose_name='open for joining')),
('created_at', models.DateTimeField(auto_now_add=True)),
('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='classrooms', to=settings.AUTH_USER_MODEL, verbose_name='teacher')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=120, verbose_name="name")),
(
"school",
models.CharField(blank=True, max_length=160, verbose_name="school"),
),
(
"join_code",
models.CharField(
editable=False,
help_text="Shareable code students use to self-join.",
max_length=8,
unique=True,
verbose_name="join code",
),
),
(
"join_password",
models.CharField(
blank=True,
default="",
help_text="Optional second factor for joining; empty = code only.",
max_length=128,
verbose_name="join password (hashed)",
),
),
(
"is_open",
models.BooleanField(
default=True,
help_text="Closed classes reject new self-joins; members stay.",
verbose_name="open for joining",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"teacher",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="classrooms",
to=settings.AUTH_USER_MODEL,
verbose_name="teacher",
),
),
],
options={
'verbose_name': 'classroom',
'verbose_name_plural': 'classrooms',
'ordering': ['-created_at'],
"verbose_name": "classroom",
"verbose_name_plural": "classrooms",
"ordering": ["-created_at"],
},
),
migrations.CreateModel(
name='Assignment',
name="Assignment",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title_i18n', models.JSONField(help_text="Assignment title per locale, e.g. {'en':…,'ro':…}", verbose_name='title (i18n)')),
('description', models.TextField(blank=True, verbose_name='description')),
('available_from', models.DateTimeField(verbose_name='available from')),
('due_at', models.DateTimeField(verbose_name='due at')),
('late_until', models.DateTimeField(blank=True, null=True, verbose_name='late until')),
('is_exam', models.BooleanField(default=False, help_text='Flag only in this increment; lockdown lands with EDU-4.', verbose_name='exam mode')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('classroom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignments', to='classroom.classroom')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"title_i18n",
models.JSONField(
help_text="Assignment title per locale, e.g. {'en':…,'ro':…}",
verbose_name="title (i18n)",
),
),
(
"description",
models.TextField(blank=True, verbose_name="description"),
),
("available_from", models.DateTimeField(verbose_name="available from")),
("due_at", models.DateTimeField(verbose_name="due at")),
(
"late_until",
models.DateTimeField(
blank=True, null=True, verbose_name="late until"
),
),
(
"is_exam",
models.BooleanField(
default=False,
help_text="Flag only in this increment; lockdown lands with EDU-4.",
verbose_name="exam mode",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"classroom",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="assignments",
to="classroom.classroom",
),
),
],
options={
'verbose_name': 'assignment',
'verbose_name_plural': 'assignments',
'ordering': ['-available_from'],
"verbose_name": "assignment",
"verbose_name_plural": "assignments",
"ordering": ["-available_from"],
},
),
migrations.CreateModel(
name='AssignmentProblem',
name="AssignmentProblem",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField(default=0, help_text='Display order (0 = A).', verbose_name='order')),
('label', models.CharField(blank=True, help_text='A, B, C…; auto-assigned.', max_length=8, verbose_name='label')),
('points', models.PositiveIntegerField(default=100, help_text='Max points inside this assignment.', verbose_name='points')),
('assignment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignment_problems', to='classroom.assignment')),
('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='assignment_problems', to='problems.problem')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"order",
models.PositiveIntegerField(
default=0,
help_text="Display order (0 = A).",
verbose_name="order",
),
),
(
"label",
models.CharField(
blank=True,
help_text="A, B, C…; auto-assigned.",
max_length=8,
verbose_name="label",
),
),
(
"points",
models.PositiveIntegerField(
default=100,
help_text="Max points inside this assignment.",
verbose_name="points",
),
),
(
"assignment",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="assignment_problems",
to="classroom.assignment",
),
),
(
"problem",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="assignment_problems",
to="problems.problem",
),
),
],
options={
'verbose_name': 'assignment problem',
'verbose_name_plural': 'assignment problems',
'ordering': ['assignment', 'order', 'id'],
'constraints': [models.UniqueConstraint(fields=('assignment', 'problem'), name='unique_assignment_problem')],
"verbose_name": "assignment problem",
"verbose_name_plural": "assignment problems",
"ordering": ["assignment", "order", "id"],
"constraints": [
models.UniqueConstraint(
fields=("assignment", "problem"),
name="unique_assignment_problem",
)
],
},
),
migrations.CreateModel(
name='ClassMembership',
name="ClassMembership",
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('joined_at', models.DateTimeField(auto_now_add=True)),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='class_memberships', to=settings.AUTH_USER_MODEL)),
('classroom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='classroom.classroom')),
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("joined_at", models.DateTimeField(auto_now_add=True)),
(
"student",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="class_memberships",
to=settings.AUTH_USER_MODEL,
),
),
(
"classroom",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="memberships",
to="classroom.classroom",
),
),
],
options={
'verbose_name': 'class membership',
'verbose_name_plural': 'class memberships',
'ordering': ['joined_at'],
'constraints': [models.UniqueConstraint(fields=('classroom', 'student'), name='unique_classroom_student')],
"verbose_name": "class membership",
"verbose_name_plural": "class memberships",
"ordering": ["joined_at"],
"constraints": [
models.UniqueConstraint(
fields=("classroom", "student"), name="unique_classroom_student"
)
],
},
),
]
Loading
Loading