Skip to content

Conversation

@devin-ai-integration
Copy link

@devin-ai-integration devin-ai-integration bot commented Jan 12, 2026

Add StudentTask model for task archive feature

Summary

This PR implements a task archive feature for training programs where students only see tasks they've encountered in training days or that were manually assigned by an admin.

Database changes:

  • New StudentTask model to track which tasks each student has access to
  • Fields: student_id, task_id, source_training_day_id (null = manual assignment), assigned_at
  • SQL migration in update_from_1.5.sql (no version bump - will be merged with training_program branch)

Core functionality:

  • When a student starts a training day, visible tasks (based on their tags) are automatically added to their StudentTask records
  • Training program overview now filters tasks based on StudentTask records only
  • Score calculations only include tasks in the student's archive
  • Tasks are hidden from sidebar in training programs (students access tasks via the archive)
  • Access control blocks direct URL access to tasks without StudentTask records

Admin UI:

  • New page to view/manage a student's task archive (accessible from student details page)
  • Manual task assignment to individual students
  • "Assign Task to a Group" feature (bulk assign by tag) with tagify dropdown
  • Students list page shows task archive progress: "50.0% (100.0/200.0) [details]"

Updates since last revision

  • TypeError fix: Fixed critical bug where StudentTask.__init__() was receiving foreign key columns as constructor arguments. CMS's custom Base.__init__ skips foreign key columns, so student_id, task_id, and source_training_day_id must be set as attributes after instantiation. Fixed in all 3 places where StudentTask is created.
  • Sidebar visibility: Tasks are now hidden from the sidebar in training programs (contest.html)
  • Access control: Added StudentTask record check in can_access_task() to block direct URL access to unassigned tasks
  • Version numbering: Removed version bump (stays at 49) and deleted update_50.py since training_program branch will be merged as a whole
  • Score optimization: Replaced submission iteration with ParticipationTaskScore cache lookup for efficient score calculation
  • UI improvements: Changed "Bulk Assign" terminology to "Assign Task to a Group" and replaced datalist with tagify component for tag selection
  • Shared utility function: Extracted calculate_task_archive_progress() into cms/server/util.py - now used by both admin students page AND contest training program overview page
  • Students page progress column: Updated format to "50.0% (100.0/200.0) [details]" with inline link; fixed column names to "First Name" and "Last Name"

Review & Testing Checklist for Human

  • Start button: Test that pressing "start" on a training day works without TypeError and adds tasks to student's archive
  • Access control: Test that direct URL access to tasks (e.g., /tasks/taskname/description) returns 404 for tasks not in student's archive
  • Score calculation consistency: Verify scores shown on admin students page match scores on student's training program overview page (both use same shared utility now)
  • Sidebar behavior: Confirm tasks don't appear in sidebar for training program contests
  • Students page progress: Verify the progress column shows correct percentage/score in format "X% (Y/Z) [details]" and the link works

Recommended test plan:

  1. Create a training program with some tasks
  2. Add a student and verify they see no tasks in the archive
  3. Check the students list page - should show "No tasks [details]" in progress column
  4. Try accessing a task URL directly - should get 404
  5. Have the student start a training day - verify no TypeError and tasks are added to archive
  6. Check the students list page again - should now show percentage and score with [details] link
  7. Log in as the student and check training program overview - scores should match admin page
  8. Manually assign a task via admin UI and verify it appears and is now accessible
  9. Test "Assign Task to a Group" with tagify dropdown

Notes

  • The source_training_day_id field is nullable: null means manual assignment, set means the task was assigned when the student started that training day
  • Important: CMS's Base.__init__ skips foreign key columns - when creating StudentTask objects, foreign keys must be set as attributes after instantiation, not passed to constructor
  • YAML export/import for StudentTask was not implemented - may need to be added if dump/restore functionality is required
  • Score calculation uses ParticipationTaskScore cache - ensure scoring service is running to keep cache updated

Link to Devin run: https://app.devin.ai/sessions/08206c8f40124bdba9d0302ea6a6792d
Requested by: Ron Ryvchin (@ronryv)

Summary by CodeRabbit

  • New Features
    • Student task archive: assign, view, and remove tasks per student with timestamps and source info.
    • Bulk-assign tasks by student tag and new admin pages to manage per-student assignments.
    • Student list shows per-student "Task Archive Progress"; new utility computes progress and task details.
    • Training-program contests now enforce assignment-based access and automatically add training-day tasks to students.
  • Database Migration
    • Adds a student_tasks table to track per-student assignments.

✏️ Tip: You can customize this high-level summary in your review settings.

This commit implements the task archive feature for training programs:

Database changes:
- Add StudentTask model to track which tasks each student has access to
- Add student_tasks relationship to Student and Task models
- Add SQL migration for student_tasks table (version 50)
- Add update_50.py for dump imports

Core functionality:
- When a student starts a training day, visible tasks are automatically
  added to their StudentTask records
- Task archive now filters tasks based on StudentTask records only
- Score calculations only include tasks in the student's archive

Admin UI:
- Add StudentTasksHandler to view/manage student's task archive
- Add AddStudentTaskHandler to manually assign tasks to students
- Add RemoveStudentTaskHandler to remove tasks from student's archive
- Add BulkAssignTaskHandler to assign tasks to all students with a tag
- Add student_tasks.html template for viewing/managing student tasks
- Add bulk_assign_task.html template for bulk task assignment
- Add link to task archive from student details page
- Add link to bulk assign from students list page

Co-Authored-By: Ron Ryvchin <[email protected]>
@devin-ai-integration
Copy link
Author

devin-ai-integration bot commented Jan 12, 2026

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@coderabbitai
Copy link

coderabbitai bot commented Jan 12, 2026

📝 Walkthrough

Walkthrough

Adds a StudentTask ORM and DB migration; links Student/Task to StudentTask; admin UI, handlers, and routes to add/remove/bulk-assign student tasks; automatic assignment of training-day tasks on contest start; task-archive progress calculation and training-program access checks requiring StudentTask records.

Changes

Cohort / File(s) Summary
DB model & exports
cms/db/__init__.py, cms/db/student_task.py, cms/db/student.py, cms/db/task.py
New StudentTask model (student_tasks table) added and exported; Student and Task gain student_tasks relationships; TYPE_CHECKING imports adjusted.
DB migration
cmscontrib/updaters/update_from_1.5.sql
Creates student_tasks table, sequence, PK, UNIQUE(student_id,task_id), FKs (students,tasks,training_days) and indexes.
Admin handlers & routes
cms/server/admin/handlers/__init__.py, .../trainingprogram.py
Adds StudentTasksHandler, AddStudentTaskHandler, RemoveStudentTaskHandler, BulkAssignTaskHandler; integrates progress computation into student listing; adds add/remove/bulk POST flows and routing.
Admin templates
cms/server/admin/templates/bulk_assign_task.html, .../student_tasks.html, .../student.html, .../training_program_students.html
New bulk-assign UI, per-student task archive UI, student page link to archive, and training-program students table shows Task Archive Progress and bulk-assign link.
Contest handlers & templates
cms/server/contest/handlers/main.py, .../contest.py, .../trainingprogram.py, cms/server/contest/templates/contest.html
On contest start, visible training-day tasks added to student archive; training-program task access now requires StudentTask; progress display replaced with calculate_task_archive_progress; template guards updated.
Utilities
cms/server/util.py
New calculate_task_archive_progress(student, participation, contest, include_task_details=False) returning total_score, max_score, percentage, task_count and optional per-task details.
Import/config
cmscontrib/importing.py
Loader/update spec updated to ignore Task.student_tasks (not updated by importer).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant StartHandler
    participant ParticipationDB as Participation
    participant StudentDB as Student
    participant TrainingDay
    participant TaskDB as Task
    participant StudentTaskDB as StudentTask

    User->>StartHandler: POST /contest/start
    StartHandler->>ParticipationDB: update starting_time and metadata
    StartHandler->>StudentDB: query Student by user + training_program
    alt Student exists
        StartHandler->>TrainingDay: get visible tasks
        TrainingDay-->>StartHandler: tasks[]
        loop for each task
            StartHandler->>StudentTaskDB: exists(student,task)?
            alt not exists
                StartHandler->>StudentTaskDB: insert StudentTask(student, task, source_training_day, assigned_at)
            end
        end
    else Student missing
        StartHandler-->>StartHandler: log warning and skip
    end
Loading
sequenceDiagram
    participant Admin
    participant AdminHandler
    participant StudentDB as Student
    participant StudentTaskDB as StudentTask
    participant Notifier

    Admin->>AdminHandler: GET/POST manage tasks / bulk-assign
    AdminHandler->>StudentDB: validate student(s) / tags
    AdminHandler->>StudentTaskDB: query/create/delete assignments (dedupe)
    alt assignments changed
        AdminHandler->>Notifier: emit notification
        AdminHandler-->>Admin: success response
    else error / duplicate
        AdminHandler-->>Admin: error response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Daniel-Aga
  • Command-Master

Poem

🐰 I hopped through migrations, tidy and bright,

Assigned tasks at training-night,
Admins click, archives expand,
Progress blooms by gentle hand,
A rabbit cheers the new task light!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing a new StudentTask model as the core of a task archive feature that spans the entire changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
cms/server/contest/handlers/trainingprogram.py (1)

58-65: Consider eager loading for potential performance improvement.

The student query doesn't eagerly load student_tasks, so calculate_task_archive_progress will trigger a separate query when accessing student.student_tasks. Similarly, participation.task_scores may be lazy-loaded. For a single-user request this is acceptable, but if performance becomes a concern, consider adding joinedload():

from sqlalchemy.orm import joinedload

student = (
    self.sql_session.query(Student)
    .options(joinedload(Student.student_tasks))
    ...
)
cms/server/admin/handlers/trainingprogram.py (1)

376-384: Consider eager loading for performance optimization.

This loop calls calculate_task_archive_progress for each student, which may result in N+1 queries for large training programs. For admin UI this is acceptable, but if performance becomes an issue, consider eager loading student_tasks relationship or batching the queries.


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62778a5 and 44fc53c.

📒 Files selected for processing (17)
  • cms/db/__init__.py
  • cms/db/student.py
  • cms/db/student_task.py
  • cms/db/task.py
  • cms/server/admin/handlers/__init__.py
  • cms/server/admin/handlers/trainingprogram.py
  • cms/server/admin/templates/bulk_assign_task.html
  • cms/server/admin/templates/student.html
  • cms/server/admin/templates/student_tasks.html
  • cms/server/admin/templates/training_program_students.html
  • cms/server/contest/handlers/contest.py
  • cms/server/contest/handlers/main.py
  • cms/server/contest/handlers/trainingprogram.py
  • cms/server/contest/templates/contest.html
  • cms/server/util.py
  • cmscontrib/importing.py
  • cmscontrib/updaters/update_from_1.5.sql
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-09T19:33:59.218Z
Learnt from: ronryv
Repo: ioi-isr/cms PR: 68
File: cms/server/admin/handlers/dataset.py:1323-1387
Timestamp: 2026-01-09T19:33:59.218Z
Learning: In cms/server/admin/handlers/**/*.py, follow the existing pattern: do not emit explicit error notifications when try_commit() fails. Rely on centralized error handling and logging instead. Apply this consistently to all new and updated handlers to maintain uniform behavior and maintainability.

Applied to files:

  • cms/server/admin/handlers/__init__.py
  • cms/server/admin/handlers/trainingprogram.py
🧬 Code graph analysis (11)
cms/server/admin/handlers/__init__.py (1)
cms/server/admin/handlers/trainingprogram.py (4)
  • StudentTasksHandler (1733-1781)
  • AddStudentTaskHandler (1784-1856)
  • RemoveStudentTaskHandler (1859-1911)
  • BulkAssignTaskHandler (1914-2008)
cms/server/util.py (2)
cms/db/contest.py (1)
  • get_tasks (335-347)
cms/server/admin/handlers/trainingprogram.py (9)
  • get (48-55)
  • get (76-87)
  • get (150-152)
  • get (223-271)
  • get (353-385)
  • get (464-506)
  • get (559-609)
  • get (748-767)
  • get (910-921)
cmscontrib/importing.py (1)
cms/db/task.py (1)
  • Task (56-365)
cms/db/__init__.py (1)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/server/admin/handlers/trainingprogram.py (3)
cms/db/student.py (1)
  • Student (38-89)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/server/util.py (1)
  • calculate_task_archive_progress (196-262)
cms/server/contest/handlers/main.py (3)
cms/db/student.py (1)
  • Student (38-89)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/server/contest/handlers/contest.py (1)
  • get_visible_tasks (460-479)
cms/db/task.py (1)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/db/student.py (2)
cms/db/training_program.py (1)
  • TrainingProgram (36-81)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/server/contest/handlers/contest.py (3)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/db/training_program.py (1)
  • TrainingProgram (36-81)
cms/db/user.py (1)
  • Participation (179-345)
cms/server/contest/handlers/trainingprogram.py (3)
cms/db/user.py (1)
  • Participation (179-345)
cms/db/student.py (1)
  • Student (38-89)
cms/server/util.py (2)
  • check_training_day_eligibility (107-154)
  • calculate_task_archive_progress (196-262)
cms/db/student_task.py (4)
cms/db/base.py (1)
  • Base (67-323)
cms/db/student.py (1)
  • Student (38-89)
cms/db/task.py (1)
  • Task (56-365)
cms/db/training_day.py (1)
  • TrainingDay (63-123)
🪛 Ruff (0.14.11)
cms/server/admin/handlers/trainingprogram.py

1819-1819: Abstract raise to an inner function

(TRY301)


1819-1819: Avoid specifying long messages outside the exception class

(TRY003)


1831-1831: Abstract raise to an inner function

(TRY301)


1831-1831: Avoid specifying long messages outside the exception class

(TRY003)


1842-1842: Do not catch blind exception: Exception

(BLE001)


1951-1951: Abstract raise to an inner function

(TRY301)


1951-1951: Avoid specifying long messages outside the exception class

(TRY003)


1955-1955: Abstract raise to an inner function

(TRY301)


1955-1955: Avoid specifying long messages outside the exception class

(TRY003)


1968-1968: Abstract raise to an inner function

(TRY301)


1968-1968: Avoid specifying long messages outside the exception class

(TRY003)


1994-1994: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (31)
cmscontrib/importing.py (1)

316-317: LGTM!

Correctly excludes student_tasks from loader updates, consistent with the pattern used for other admin-managed relationships like submissions and user_tests. The comment clearly documents the rationale.

cmscontrib/updaters/update_from_1.5.sql (1)

524-567: LGTM!

The schema correctly mirrors the ORM definition:

  • Appropriate cascade behaviors (CASCADE for student/task deletion, SET NULL for training day)
  • Proper unique constraint on (student_id, task_id) prevents duplicate assignments
  • Indexes on all foreign keys support efficient queries
  • assigned_at is correctly NOT NULL for audit trail
cms/db/task.py (2)

53-53: LGTM!

Correct use of TYPE_CHECKING to avoid circular imports while enabling type hints.


328-332: LGTM!

The relationship definition is consistent with the existing patterns in this file and correctly mirrors the StudentTask.task back_populates. The passive_deletes=True properly delegates cascade handling to the database.

cms/server/admin/templates/student.html (1)

28-30: LGTM!

Clean addition that follows the existing URL patterns in this template. The task count display using student.student_tasks|length provides useful context for the admin.

cms/server/contest/templates/contest.html (1)

183-195: LGTM!

Correctly hides the task list sidebar for training program contests. This aligns with the new StudentTask-based visibility model where training program participants see tasks through their personalized task archive rather than the generic contest sidebar.

cms/db/__init__.py (1)

57-57: LGTM!

The StudentTask export and import are correctly placed alongside related training program models, following the established pattern in this module.

Also applies to: 112-112

cms/db/student.py (1)

83-89: LGTM!

The student_tasks relationship is properly configured with cascade="all, delete-orphan" and passive_deletes=True, correctly complementing the ON DELETE CASCADE foreign key constraint defined in StudentTask.student_id. The back_populates value matches the inverse relationship in StudentTask.

cms/server/admin/templates/training_program_students.html (1)

60-60: LGTM!

The progress lookup with .get(student.id, {}) safely handles missing entries, and the conditional {% if progress.task_count > 0 %} correctly handles both missing keys (empty dict fallback) and zero task counts.

Also applies to: 75-81

cms/server/contest/handlers/contest.py (1)

439-458: LGTM!

The exists() subquery efficiently validates the full authorization chain (user → participation → student → student_task → task) without transferring unnecessary data. The multiple join conditions in and_() correctly enforce that:

  1. The task is in the student's archive
  2. The student belongs to the current user's participation
  3. The student is part of the specific training program

This provides proper access control for the task archive feature.

cms/server/contest/handlers/trainingprogram.py (1)

57-81: LGTM!

Good refactor to use the shared calculate_task_archive_progress utility, eliminating code duplication with the admin handlers. The fallback to zero values when no student record exists provides sensible default behavior.

cms/server/admin/templates/student_tasks.html (1)

1-79: LGTM!

The template is well-structured with:

  • Proper CSRF protection via xsrf_form_html
  • Permission checks disabling buttons when admin.permission_all is false
  • Correct JavaScript string escaping in the confirmation dialog (\\')
  • Clear empty state handling for when no tasks are assigned
cms/server/admin/templates/bulk_assign_task.html (2)

15-36: LGTM!

Good defensive coding with the Tagify initialization:

  • Checks that tagInput exists and Tagify is defined before use
  • enforceWhitelist: true prevents arbitrary tag injection
  • originalInputValueFormat ensures clean value submission

38-74: LGTM!

The form is properly secured with CSRF protection and permission checks. The description clearly explains the bulk assignment behavior and skip logic.

cms/server/util.py (1)

196-262: LGTM!

Well-implemented utility function with:

  • Efficient O(1) lookups via dictionaries for both student tasks and cached scores
  • Proper division-by-zero protection at line 250
  • Sensible fallback of 100.0 for max_score when active_dataset is None
  • Clean optional include_task_details parameter for flexibility
cms/server/admin/handlers/__init__.py (2)

178-181: LGTM!

New handler imports follow the established pattern and are properly grouped with other training program handlers.


341-344: LGTM!

The new routes are well-structured:

  • Follow the existing URL pattern conventions for training program resources
  • Logically grouped with other student-related routes
  • Use consistent numeric ID capture groups
cms/server/contest/handlers/main.py (3)

59-59: LGTM!

Import correctly adds Student and StudentTask for the new task archive functionality.


348-355: LGTM!

Good placement of the training day task assignment logic - it runs before the commit so all changes (starting_time, IP addresses, and student tasks) are committed atomically.


357-405: LGTM!

Well-implemented helper method:

  • Efficient O(1) lookup for existing task IDs to avoid duplicates
  • Uses get_visible_tasks() to respect tag-based visibility rules
  • Proper FK attribute assignment pattern (consistent with other handlers per the PR description)
  • Helpful debug logging for each task addition
  • Graceful handling when student record is missing (warning + early return)
cms/server/admin/handlers/trainingprogram.py (7)

30-33: LGTM!

Import changes correctly add StudentTask for the new task assignment functionality and calculate_task_archive_progress utility for computing student progress.


391-391: LGTM!

Good cleanup - removed unused variable assignment while maintaining the authorization check via safe_get_item.


1733-1781: LGTM!

Handler correctly validates the student's existence, computes available tasks efficiently using set comprehension, and sorts assigned tasks by recency.


1833-1840: LGTM on the FK workaround pattern.

The comment correctly explains that CMS Base.__init__ skips foreign key columns (verified in cms/db/base.py:108-111), so setting FK attributes after construction is the appropriate pattern here.


1859-1911: LGTM!

Handler correctly validates the chain of entities (participation → student → student_task), retrieves the task name before deletion for the notification, and follows the established error handling pattern.


1960-1979: Good batch optimization for duplicate checking.

The bulk query for already_assigned_ids efficiently avoids N+1 queries by fetching all existing assignments in a single query before iterating over matching students.


1981-1992: LGTM!

Batch creation pattern is efficient. All StudentTask records are created in a single transaction, which is appropriate for bulk admin operations.

cms/db/student_task.py (4)

18-36: LGTM!

Clear module documentation explaining the purpose of StudentTask, and appropriate use of TYPE_CHECKING to avoid circular imports.


39-51: LGTM!

The UniqueConstraint on (student_id, task_id) correctly enforces that a task can only be assigned to a student once, with a well-named constraint following CMS conventions.


53-82: LGTM!

Column definitions are well-designed:

  • Appropriate CASCADE behaviors for student/task deletion
  • SET NULL on training_day deletion preserves manually assigned tasks
  • Indexes on all FK columns optimize query performance

84-96: LGTM!

Relationships are properly configured with back_populates matching the corresponding relationships in Student and Task models. The source_training_day relationship correctly omits back_populates since TrainingDay doesn't need a reverse collection of student tasks.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

devin-ai-integration bot and others added 6 commits January 12, 2026 12:02
Fix CI failure by adding Task.student_tasks to the update specification
in update_task(). Student task assignments are managed separately via
the admin UI and should not be updated during task import.

Co-Authored-By: Ron Ryvchin <[email protected]>
…timization, UI improvements

- Hide tasks from sidebar in training programs (contest.html)
- Add access control for direct URL access to tasks in training programs (contest.py)
- Remove version number increase (keep at 49) and delete update_50.py
- Optimize score calculation to use ParticipationTaskScore cache (trainingprogram.py)
- Change 'bulk assign' terminology to 'Assign Task to a Group' and use tagify (bulk_assign_task.html)

Co-Authored-By: Ron Ryvchin <[email protected]>
CMS Base.__init__ skips foreign key columns, so student_id, task_id,
and source_training_day_id cannot be passed as constructor arguments.
Fixed all three places where StudentTask is created to set these
attributes after instantiation.

Co-Authored-By: Ron Ryvchin <[email protected]>
Shows percentage and score (e.g., '75.0% (150.0/200.0)') for each student,
plus a 'Task Archive' button linking to their task archive page.
Uses ParticipationTaskScore cache for efficient score lookup.

Co-Authored-By: Ron Ryvchin <[email protected]>
- Add calculate_task_archive_progress() utility function in cms/server/util.py
- Update admin handler to use shared utility
- Combine progress and details link: '50.0% (100.0/200.0) [details]'
- Fix column names: 'First Name' and 'Last Name' (proper capitalization)

Co-Authored-By: Ron Ryvchin <[email protected]>
…utility

Extended the utility to optionally return per-task breakdown (include_task_details=True).
Both admin and contest handlers now use the same shared logic for score calculation.

Co-Authored-By: Ron Ryvchin <[email protected]>
@ronryv
Copy link
Collaborator

ronryv commented Jan 13, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (6)
cms/server/contest/handlers/contest.py (1)

429-444: Access control implementation looks correct.

The logic properly enforces that:

  1. A Student record must exist linking the user to the training program
  2. The task must be in the student's task archive (student_tasks)

One minor consideration: the query retrieves the full Student object and then accesses student.student_tasks, which triggers a second query if not eagerly loaded. For a single student this is acceptable, but if this becomes a hot path, consider using a subquery or exists() check directly.

🔧 Optional: More efficient task access check using exists()

If performance becomes a concern, you could check task access directly without loading all student tasks:

from sqlalchemy import exists
from cms.db import StudentTask

# For training programs, check if student has a StudentTask record
if self.training_program is not None:
    has_access = self.sql_session.query(
        exists()
        .where(StudentTask.task_id == task.id)
        .where(StudentTask.student_id == Student.id)
        .where(Student.participation_id == Participation.id)
        .where(Participation.contest_id == self.contest.id)
        .where(Participation.user_id == self.current_user.user_id)
        .where(Student.training_program_id == self.training_program.id)
    ).scalar()
    return has_access
cms/server/util.py (2)

44-44: Unused import: ParticipationTaskScore is not directly referenced.

The import is added but ParticipationTaskScore is not explicitly used in the code—it's accessed indirectly through participation.task_scores. If this import is for documentation purposes or future type annotations, consider adding a type hint to make the usage explicit; otherwise, it can be removed.


225-237: Potential inconsistency between task_count and score accumulation.

task_count is set to len(student_task_ids) (all tasks assigned to the student), but max_score and total_score only accumulate for tasks that are also in contest.get_tasks(). If a student has tasks assigned that don't belong to the contest, the count will be inflated relative to the score totals.

If this is intentional (e.g., counting all assigned tasks regardless of contest membership), consider adding a clarifying comment. Otherwise, consider counting tasks inside the loop:

Suggestion to count tasks in-loop
     total_score = 0.0
     max_score = 0.0
-    task_count = len(student_task_ids)
+    task_count = 0
     task_scores = [] if include_task_details else None
 
     for task in contest.get_tasks():
         if task.id not in student_task_ids:
             continue
+        task_count += 1
         max_task_score = task.active_dataset.score_type_object.max_score \
             if task.active_dataset else 100.0
cms/server/admin/handlers/trainingprogram.py (3)

376-383: Consider eager loading to avoid N+1 queries.

This loop accesses student.student_tasks and student.participation.task_scores for each student, which may trigger lazy loading queries. For training programs with many students, consider eager loading these relationships:

from sqlalchemy.orm import joinedload

students = (
    self.sql_session.query(Student)
    .filter(Student.training_program == training_program)
    .options(
        joinedload(Student.student_tasks),
        joinedload(Student.participation).joinedload(Participation.task_scores)
    )
    .all()
)

for student in students:
    student_progress[student.id] = calculate_task_archive_progress(...)

1960-1969: Filter students at database level for better performance.

Currently loads all students and filters in Python, bypassing the GIN index on student_tags. Use SQLAlchemy's array containment to filter at the database level:

Use database-level filtering with the GIN index
-            # Find all students with the given tag
-            students_with_tag = (
-                self.sql_session.query(Student)
-                .filter(Student.training_program == training_program)
-                .all()
-            )
-
-            # Filter to students that have the tag
-            matching_students = [
-                s for s in students_with_tag if tag in s.student_tags
-            ]
+            # Find all students with the given tag (uses GIN index)
+            matching_students = (
+                self.sql_session.query(Student)
+                .filter(Student.training_program == training_program)
+                .filter(Student.student_tags.any(tag))
+                .all()
+            )

1974-1991: Consider batch optimization for existence checks.

The loop performs a query per student to check for existing assignments. For bulk operations with many students, this could be optimized by pre-fetching all existing assignments:

Pre-fetch existing assignments to reduce queries
# Pre-fetch existing assignments for this task
existing_student_ids = set(
    row[0] for row in self.sql_session.query(StudentTask.student_id)
    .filter(StudentTask.task_id == task.id)
    .filter(StudentTask.student_id.in_([s.id for s in matching_students]))
    .all()
)

assigned_count = 0
for student in matching_students:
    if student.id not in existing_student_ids:
        student_task = StudentTask(assigned_at=make_datetime())
        student_task.student_id = student.id
        student_task.task_id = task.id
        student_task.source_training_day_id = None
        self.sql_session.add(student_task)
        assigned_count += 1
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62778a5 and 18712f7.

📒 Files selected for processing (17)
  • cms/db/__init__.py
  • cms/db/student.py
  • cms/db/student_task.py
  • cms/db/task.py
  • cms/server/admin/handlers/__init__.py
  • cms/server/admin/handlers/trainingprogram.py
  • cms/server/admin/templates/bulk_assign_task.html
  • cms/server/admin/templates/student.html
  • cms/server/admin/templates/student_tasks.html
  • cms/server/admin/templates/training_program_students.html
  • cms/server/contest/handlers/contest.py
  • cms/server/contest/handlers/main.py
  • cms/server/contest/handlers/trainingprogram.py
  • cms/server/contest/templates/contest.html
  • cms/server/util.py
  • cmscontrib/importing.py
  • cmscontrib/updaters/update_from_1.5.sql
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-09T19:33:59.218Z
Learnt from: ronryv
Repo: ioi-isr/cms PR: 68
File: cms/server/admin/handlers/dataset.py:1323-1387
Timestamp: 2026-01-09T19:33:59.218Z
Learning: In cms/server/admin/handlers/**/*.py, follow the existing pattern: do not emit explicit error notifications when try_commit() fails. Rely on centralized error handling and logging instead. Apply this consistently to all new and updated handlers to maintain uniform behavior and maintainability.

Applied to files:

  • cms/server/admin/handlers/__init__.py
  • cms/server/admin/handlers/trainingprogram.py
🧬 Code graph analysis (10)
cmscontrib/importing.py (1)
cms/db/task.py (1)
  • Task (56-365)
cms/db/__init__.py (1)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/db/student.py (2)
cms/db/training_program.py (1)
  • TrainingProgram (36-81)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/server/admin/handlers/__init__.py (1)
cms/server/admin/handlers/trainingprogram.py (4)
  • StudentTasksHandler (1733-1781)
  • AddStudentTaskHandler (1784-1856)
  • RemoveStudentTaskHandler (1859-1911)
  • BulkAssignTaskHandler (1914-2007)
cms/server/contest/handlers/main.py (3)
cms/db/student.py (1)
  • Student (38-89)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/server/contest/handlers/contest.py (1)
  • get_visible_tasks (449-468)
cms/server/util.py (4)
cms/db/student.py (1)
  • Student (38-89)
cms/db/task.py (2)
  • Task (56-365)
  • score_type_object (574-594)
cms/db/user.py (1)
  • Participation (179-345)
cms/db/scorecache.py (1)
  • ParticipationTaskScore (36-117)
cms/db/task.py (1)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/server/contest/handlers/contest.py (2)
cms/db/student.py (1)
  • Student (38-89)
cms/db/user.py (1)
  • Participation (179-345)
cms/server/admin/handlers/trainingprogram.py (6)
cms/db/student.py (1)
  • Student (38-89)
cms/db/student_task.py (1)
  • StudentTask (39-96)
cms/db/training_day.py (1)
  • TrainingDay (63-123)
cms/db/training_day_group.py (1)
  • TrainingDayGroup (37-82)
cms/server/util.py (2)
  • get_all_student_tags (64-79)
  • calculate_task_archive_progress (196-261)
cms/db/training_program.py (1)
  • TrainingProgram (36-81)
cms/db/student_task.py (4)
cms/db/base.py (1)
  • Base (67-323)
cms/db/student.py (1)
  • Student (38-89)
cms/db/task.py (1)
  • Task (56-365)
cms/db/training_day.py (1)
  • TrainingDay (63-123)
🪛 Ruff (0.14.10)
cms/server/admin/handlers/trainingprogram.py

1819-1819: Abstract raise to an inner function

(TRY301)


1819-1819: Avoid specifying long messages outside the exception class

(TRY003)


1831-1831: Abstract raise to an inner function

(TRY301)


1831-1831: Avoid specifying long messages outside the exception class

(TRY003)


1842-1842: Do not catch blind exception: Exception

(BLE001)


1951-1951: Abstract raise to an inner function

(TRY301)


1951-1951: Avoid specifying long messages outside the exception class

(TRY003)


1955-1955: Abstract raise to an inner function

(TRY301)


1955-1955: Avoid specifying long messages outside the exception class

(TRY003)


1972-1972: Abstract raise to an inner function

(TRY301)


1972-1972: Avoid specifying long messages outside the exception class

(TRY003)


1993-1993: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (30)
cmscontrib/importing.py (1)

316-317: LGTM!

The addition of Task.student_tasks: False to the update spec is consistent with the existing pattern for relationships managed separately (e.g., Task.training_day, Task.participation_scores). This correctly prevents the loader from overwriting admin-managed student task assignments during imports.

cms/db/student_task.py (1)

39-96: Well-structured ORM model.

The StudentTask model is well-designed with appropriate constraints:

  • Composite unique constraint on (student_id, task_id) prevents duplicate assignments
  • Cascade delete behaviors are correct: deleting a student or task removes the assignment, while deleting a training day preserves the assignment (sets source_training_day_id to NULL)
  • All foreign key columns are properly indexed

Note: The assigned_at column has no default value, so callers must provide it explicitly (e.g., using datetime.utcnow()). This appears intentional based on the PR's mention of fixing a TypeError by setting foreign keys after instantiation.

cms/db/student.py (1)

83-89: LGTM!

The student_tasks relationship is correctly configured:

  • cascade="all, delete-orphan" ensures task assignments are deleted when the student is deleted
  • passive_deletes=True works correctly with the ON DELETE CASCADE foreign key constraint in StudentTask
  • back_populates="student" correctly references the inverse relationship in StudentTask
cms/db/task.py (1)

328-333: LGTM!

The student_tasks relationship follows the same pattern as other one-to-many relationships in Task (e.g., participation_scores, submissions). The cascade and passive_deletes configuration ensures proper cleanup when a task is deleted.

cms/db/__init__.py (2)

57-57: LGTM!

StudentTask is correctly added to __all__ in the contest section alongside related entities (TrainingDay, TrainingDayGroup).


112-112: LGTM!

The import is properly placed and follows the existing module organization pattern.

cms/server/admin/templates/student.html (1)

28-31: LGTM!

The Task Archive link is correctly placed and uses the student.student_tasks relationship to display the count. The URL construction follows the existing patterns in this template.

cmscontrib/updaters/update_from_1.5.sql (1)

524-567: LGTM!

The migration correctly defines the student_tasks table with appropriate:

  • Unique constraint on (student_id, task_id) to prevent duplicate assignments
  • Cascading deletes for student_id and task_id FKs to maintain referential integrity
  • ON DELETE SET NULL for source_training_day_id which preserves manually assigned tasks when training days are deleted
  • Indexes on all foreign key columns for query performance
cms/server/contest/templates/contest.html (1)

183-195: LGTM!

The condition correctly hides the task list from the sidebar for training programs, where task visibility is now controlled through the Task Archive feature. Students will access their assigned tasks through the dedicated Task Archive interface instead.

cms/server/admin/templates/training_program_students.html (4)

14-17: LGTM!

The bulk assign task link is correctly placed and follows the existing URL pattern conventions.


51-54: LGTM!

Header capitalization is now consistent ("First Name", "Last Name"), and the new "Task Archive Progress" column header is appropriately placed.


75-81: LGTM!

The progress display logic correctly handles both cases:

  • When tasks exist: shows percentage, scores, and details link
  • When no tasks: shows "No tasks" with details link for manual assignment

The format strings "%.1f" provide consistent one-decimal precision.


60-60: Verified: student_progress is correctly passed from the handler.

The handler in trainingprogram.py properly initializes and populates student_progress with the return value of calculate_task_archive_progress(), which returns a dict containing all expected keys: task_count, percentage, total_score, and max_score. The template's safe access with .get(student.id, {}) handles missing student entries correctly.

cms/server/contest/handlers/contest.py (1)

415-417: LGTM!

The docstring update accurately describes the new access control behavior for training programs.

cms/server/admin/templates/bulk_assign_task.html (2)

53-58: The placeholder value "null" is a string literal.

The value="null" on line 54 is a string. Ensure the backend handler (BulkAssignTaskHandler.post) validates this as an invalid selection. Based on the relevant code snippets, the handler appears to check for "null" string, so this is consistent.


15-36: LGTM! Tagify initialization is well-configured.

The JavaScript properly checks for Tagify availability before initialization, uses enforceWhitelist: true to restrict input to valid tags, and configures the dropdown appropriately.

cms/server/admin/handlers/__init__.py (2)

178-181: LGTM! Handler exports follow existing patterns.

The new handler imports are properly added and maintain consistency with the existing export style.


341-344: LGTM! URL routes are well-structured.

The new routes follow the established URL pattern conventions for the training program section. The route hierarchy (/training_program/{id}/student/{id}/tasks, /training_program/{id}/bulk_assign_task) is consistent with existing patterns.

cms/server/admin/templates/student_tasks.html (1)

1-79: LGTM! Template is well-structured with proper security controls.

The template correctly implements:

  • CSRF protection on all forms
  • Permission checks on submit buttons
  • Confirmation dialog for destructive remove action
  • Clean table layout with relevant information (task, source, date, actions)
cms/server/contest/handlers/trainingprogram.py (2)

57-65: LGTM! Student lookup query is correct.

The query properly joins Student to Participation and applies the necessary filters to find the student record for the current user within the specific training program.


67-81: Good refactor to use shared utility.

Using calculate_task_archive_progress centralizes the score calculation logic, improving maintainability. The fallback to zeros when no student record exists is appropriate.

cms/server/contest/handlers/main.py (3)

348-351: LGTM! Training day task assignment is correctly integrated.

The helper method is called before the commit, ensuring all StudentTask records are persisted atomically with the participation start time update.


391-405: LGTM! Correct pattern for creating StudentTask with FK constraints.

The code correctly:

  1. Uses a set for O(1) lookup of existing task IDs
  2. Creates StudentTask with assigned_at first, then sets FK attributes (per CMS ORM pattern noted in comments)
  3. Adds informative logging for debugging

380-385: This warning and graceful degradation pattern is intentional and appropriate.

The codebase consistently handles missing Student records by degrading gracefully. Student records are created on-demand by admin handlers, not automatically when users participate. This design is reflected across multiple handlers: contest.py (returns False), trainingprogram.py (shows zero scores), and main.py (returns early). The warning appropriately alerts about the missing Student record while the early return prevents null-pointer errors when accessing student.student_tasks.

cms/server/util.py (1)

196-261: LGTM on the overall implementation.

The function is well-documented and handles edge cases appropriately (null active_dataset, empty max_score). The use of dictionaries for O(1) lookups is efficient.

cms/server/admin/handlers/trainingprogram.py (5)

31-33: LGTM on imports.

The new imports correctly bring in StudentTask and calculate_task_archive_progress needed for the new handlers.


1733-1781: LGTM on StudentTasksHandler.

The handler correctly validates the participation and student, computes available tasks for the dropdown, and sorts assigned tasks by assignment date.


1833-1840: LGTM on StudentTask creation pattern.

The comment correctly documents the CMS Base.init behavior that requires FK columns to be set after instantiation. This matches the PR fix for the TypeError issue.


1859-1911: LGTM on RemoveStudentTaskHandler.

The handler correctly validates the chain of participation → student → student_task before deletion. The success notification provides good feedback to the admin.


1842-1847: Exception handling follows established codebase pattern.

The broad except Exception clauses align with the existing pattern in this file (e.g., lines 133, 399, 673). Based on learnings, this is the standard approach in cms/server/admin/handlers/**/*.py. The static analysis hints (BLE001, TRY301, TRY003) can be safely disregarded for consistency.

Also applies to: 1993-1998

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ronryv ronryv force-pushed the devin/1768218330-student-task-archive branch from c656f98 to 44fc53c Compare January 14, 2026 08:56
@ronryv
Copy link
Collaborator

ronryv commented Jan 14, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ronryv
Copy link
Collaborator

ronryv commented Jan 14, 2026

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

✅ Actions performed

Full review triggered.

@ronryv
Copy link
Collaborator

ronryv commented Jan 15, 2026

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

✅ Actions performed

Full review triggered.

@ronryv ronryv merged commit d6c69f6 into training_program Jan 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants