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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""failed_submissions and next_grading_allowed_at columns added to grading_job table

Revision ID: 3836d6b8e3f6
Revises: 4baf7c606f77
Create Date: 2025-11-09 13:03:28.701620

"""
from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = '3836d6b8e3f6'
down_revision: Union[str, Sequence[str], None] = '4baf7c606f77'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('exercise_progress', sa.Column('skipped', sa.BOOLEAN(), nullable=False))
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('exercise_progress', 'skipped')
# ### end Alembic commands ###
22 changes: 21 additions & 1 deletion app/api/v1/grading_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime
from uuid import uuid4

import sqlalchemy as sa
from aio_pika import Message
from aio_pika.abc import AbstractRobustChannel
from fastapi import APIRouter, Depends, HTTPException
Expand All @@ -18,6 +19,8 @@
from app.mq.message_queue import get_mq_channel
from app.util import get_datetime_now

INITIAL_JOB_STATUS = "pending"

router = APIRouter(
prefix="/grading-jobs",
tags=["grading jobs"],
Expand All @@ -30,10 +33,12 @@ async def submit_grading_job(job_msg: dict, session: AsyncSession, ch: AbstractR
id=job_msg["job_id"],
tan_code=job_msg["tan_code"],
exercise_id=job_msg["exercise_id"],
status="pending",
status=INITIAL_JOB_STATUS,
started=now
))

await session.commit()

job_msg["job_id"] = str(job_msg["job_id"])

exchange = await ch.get_exchange(MQ_EXCHANGE_NAME)
Expand All @@ -53,6 +58,21 @@ async def create_submission(new_submission: ExerciseSubmission, session: AsyncSe
if not exercise_progress:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Tried to submit to an inactive exercise.")

if exercise_progress.next_grading_allowed_at and exercise_progress.next_grading_allowed_at > now:
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"Next grading allowed at {exercise_progress.next_grading_allowed_at}")

stmt = select(GradingJob).where(sa.and_(GradingJob.exercise_id == new_submission.exercise_id,
GradingJob.tan_code == new_submission.tan_code,
GradingJob.status == INITIAL_JOB_STATUS))

result = await session.execute(stmt)
grading_job = result.scalars().first()

if grading_job:
raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail="Previous grading job still in progress!")

try:
job_msg = {
"job_id": uuid4(),
Expand Down
2 changes: 2 additions & 0 deletions app/db/model/exercise.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class ExerciseProgress(Base):
start_time = sa.Column(sa.DateTime(timezone=True), nullable=False)
end_time = sa.Column(sa.DateTime(timezone=True), nullable=True)
skipped = sa.Column(sa.BOOLEAN, nullable=False)
failed_submissions = sa.Column(sa.INTEGER, nullable=False, default=0)
next_grading_allowed_at = sa.Column(sa.DateTime(timezone=True), nullable=True)


class Competition(Base):
Expand Down
2 changes: 1 addition & 1 deletion app/mq/message_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

async def get_mq_channel() -> AbstractRobustChannel:
uri = f"amqp://{MQ_USER}:{MQ_PWD}@{MQ_URL}:{MQ_PORT}"
logging.debug('connect to: ', uri)
logging.debug(f'connect to: {uri}')
connection = await aio_pika.connect_robust(uri)

async with connection:
Expand Down
66 changes: 64 additions & 2 deletions tests/test_grading_job.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import asyncio
from datetime import timezone, datetime
from datetime import datetime
from unittest.mock import MagicMock, ANY, AsyncMock

from aio_pika.abc import AbstractRobustChannel
Expand Down Expand Up @@ -39,7 +39,7 @@ async def get_mq_connection_override() -> AbstractRobustChannel:

def get_datetime_now_override():
def now():
yield datetime(2026, 10, 7, 19, 31, 0, tzinfo=timezone.utc)
yield datetime(2026, 10, 7, 19, 37, 0)

return now

Expand Down Expand Up @@ -86,3 +86,65 @@ async def get_mq_connection_override() -> AbstractRobustChannel:
assert response.status_code == status.HTTP_400_BAD_REQUEST
channel_mock.get_exchange.assert_not_awaited()
exchange_mock.publish.assert_not_awaited()

def test_early_grading_job_submission(self):
channel_mock, exchange_mock = setup_mocks()

async def get_mq_connection_override() -> AbstractRobustChannel:
yield channel_mock # noqa

def get_datetime_now_override():
def now():
yield datetime(2025, 10, 7, 19, 31, 0)

return now

app.dependency_overrides[get_datetime_now] = get_datetime_now_override()
app.dependency_overrides[get_session] = get_override_dependency(self.engine)
app.dependency_overrides[get_mq_channel] = get_mq_connection_override

client = TestClient(app)

exercise_submission = {
"tan_code": "test-tan-1",
"exercise_id": 2,
"solution_code": "addi r0 r0 r0"
}

response = client.post("/grading-jobs", json=exercise_submission)

print(response.json())

assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS
assert response.json()["detail"] == "Next grading allowed at 2025-10-07 19:35:00"

def test_post_job_with_pending_job(self):
channel_mock, exchange_mock = setup_mocks()

async def get_mq_connection_override() -> AbstractRobustChannel:
yield channel_mock # noqa

def get_datetime_now_override():
def now():
yield datetime(2025, 10, 7, 19, 50, 0)

return now

app.dependency_overrides[get_datetime_now] = get_datetime_now_override()
app.dependency_overrides[get_session] = get_override_dependency(self.engine)
app.dependency_overrides[get_mq_channel] = get_mq_connection_override

client = TestClient(app)

exercise_submission = {
"tan_code": "test-tan-4",
"exercise_id": 1,
"solution_code": "addi r0 r0 r0"
}

response = client.post("/grading-jobs", json=exercise_submission)

print(response.json())

assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS
assert response.json()["detail"] == "Previous grading job still in progress!"
13 changes: 11 additions & 2 deletions tests/util/db_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine, async_sessionmaker

from app.db.database import Base
from app.db.model import Exercise, Tan, LoggingEvent
from app.db.model import Exercise, Tan, LoggingEvent, GradingJob
from app.db.model.exercise import ExerciseProgress, Competition, TestCase
from tests.util.demo_data import COMPETITIONS, TANS, EXERCISES, EXERCISE_PROGRESS_ENTRIES, EXERCISE_TEST_CASES, \
LOGGING_EVENTS
LOGGING_EVENTS, GRADING_JOBS

DB_URI = "sqlite+aiosqlite:///:memory:"

Expand Down Expand Up @@ -45,6 +45,9 @@ async def insert_demo_data(session_factory: async_sessionmaker):
for test_case in EXERCISE_TEST_CASES[exercise_id]:
await insert_exercise_test_case(session, exercise_id, test_case)

for grading_job in GRADING_JOBS:
await insert_grading_job(session, grading_job)


async def insert_exercise(session: AsyncSession, exercise: dict) -> None:
exercise = Exercise(**exercise)
Expand Down Expand Up @@ -81,3 +84,9 @@ async def insert_exercise_test_case(session: AsyncSession, exercise_id: int, tes
test_case = TestCase(exercise_id=exercise_id, **test_case)
session.add(test_case)
await session.commit()


async def insert_grading_job(session: AsyncSession, grading_job: dict) -> None:
grading_job = GradingJob(**grading_job)
session.add(grading_job)
await session.commit()
27 changes: 26 additions & 1 deletion tests/util/demo_data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime, timezone
from uuid import uuid4

COMPETITIONS = [
{
Expand All @@ -24,6 +25,10 @@
"competition_id": 1,
"valid_from": datetime(2025, 10, 7, 18, 0, 0, tzinfo=timezone.utc),
},
{
"code": "test-tan-4",
"competition_id": 1,
},
{
"code": "logging-test-tan",
"competition_id": 1,
Expand Down Expand Up @@ -94,7 +99,8 @@
"exercise_id": 2,
"start_time": datetime(2025, 10, 7, 19, 30, 0, tzinfo=timezone.utc),
"end_time": None,
"skipped": False
"skipped": False,
"next_grading_allowed_at": datetime(2025, 10, 7, 19, 35, 0, tzinfo=timezone.utc)
},
{
"id": 3,
Expand All @@ -112,6 +118,25 @@
"end_time": datetime(2025, 10, 7, 20, 0, 0, tzinfo=timezone.utc),
"skipped": False
},
{
"id": 5,
"tan_code": "test-tan-4",
"exercise_id": 1,
"start_time": datetime(2025, 10, 7, 19, 0, 0, tzinfo=timezone.utc),
"end_time": None,
"skipped": False
},

]

GRADING_JOBS = [
{
"id": uuid4(),
"tan_code": "test-tan-4",
"exercise_id": 1,
"status": "pending",
"started": datetime(2025, 10, 7, 18, 0, 0, tzinfo=timezone.utc),
}
]

EXERCISE_TEST_CASES = {
Expand Down