Skip to content

Conversation

@devin-ai-integration
Copy link

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

Add archive training day feature with combined ranking

Summary

This PR implements the archive training day feature for training programs. When a training day is archived:

  • Attendance data is extracted and stored (participation status, location based on IP, delay time, delay reasons)
  • Ranking data is extracted and stored (all student tags, task scores, score history for graphs)
  • The contest and participations are permanently deleted
  • The training day remains with name/description/start_time preserved

New database models ArchivedAttendance and ArchivedStudentRanking store the extracted data. The TrainingDay model now has nullable contest_id (SET NULL on delete) and new name/description/start_time/archived_tasks_data fields.

UI changes include:

  • Training days page now separates active vs archived training days
  • Archive button on each active training day
  • Archive confirmation page with class IP selection
  • New "Attendance" tab in training program sidebar showing attendance data across all archived training days
  • Archived training days table shows start time with sortable column
  • New "Combined Ranking" tab showing rankings across all archived training days

Updates since last revision

  • Fix graph scale and submission time display in history view: The contest begin/end times were incorrectly set to absolute timestamps, but history and submission times are stored as offsets from contest start. This caused graphs to have incorrect scale and submission times to display incorrectly. Fixed by setting contest begin to 0 and calculating end from the max history time.

Previous updates (still relevant)

  • Add StudentTask records during archiving: When archiving, StudentTask records are now created for visible tasks that the student doesn't already have. This allows students who missed the training to still submit those tasks from home via the training program management contest, mirroring the behavior of the start button.
  • Archive visible tasks based on student tags: Changed archiving to determine which tasks should be visible to each student based on their tags (using can_access_task), rather than only including tasks where the student already has a StudentTask record. This ensures students who missed the start button still get the training day's tasks in their archive with 0 scores.
  • Simplified data model: Removed redundant visible_task_ids field from ArchivedStudentRanking. The presence of a task_id key in task_scores now indicates the task was visible to that student (including 0 scores).
  • Moved tasks_data to TrainingDay: Task metadata (archived_tasks_data) is now stored once on TrainingDay instead of duplicated per-student in ArchivedStudentRanking. This preserves the original scoring scheme at archive time.
  • Added submissions field: ArchivedStudentRanking now stores submission data for each task in RWS format for rendering submission tables in the history view.
  • Updated combined ranking handlers: Now use task_scores.keys() for visibility and td.archived_tasks_data for task metadata.
  • Updated templates: Combined ranking table now shows "N/A" for inaccessible tasks based on task_scores keys.
  • Switched to get_cached_score_entry() for archiving: Per feedback, changed from using task_score() to get_cached_score_entry() which efficiently uses the score cache.
  • Combined Ranking page: Added new page accessible from sidebar that displays rankings for all archived training days with date range filtering.
  • Combined Ranking history view: Each student has a "[history]" link that opens a detail page with score/rank graphs.
  • Student tags storage: Changed to storing ALL student tags as PostgreSQL ARRAY(Unicode) with a GIN index.
  • Location defaults to "home": When no class IPs are selected during archiving, all participants are now marked as "home".

Review & Testing Checklist for Human

  • Test history view graphs and submission times - Click "[history]" for a student with submissions and verify: (1) the score/rank graphs display with correct scale (x-axis should show time from 0 to contest duration), (2) submission times in the table display correctly (not negative or extremely large numbers).

  • Verify StudentTask creation during archiving - Archive a training day where some students missed the start button. Check that StudentTask records are created for those students' visible tasks (based on tags). Verify they can then submit those tasks from the training program management contest.

  • Test archiving with students who missed start button - Create a training day, have some students NOT press start, then archive. Verify those students still get the training day's tasks in their archive with 0 scores based on their tags.

  • Verify tag-based task visibility - Create tasks with visible_to_tags restrictions. Archive and verify students only get tasks matching their tags (same behavior as the start button).

  • Test combined ranking table - Check that tasks visible to at least one student appear as columns, scores display correctly, and "N/A" shows for inaccessible tasks.

Recommended test plan:

  1. Create a training day with multiple students and tasks (some tasks with visible_to_tags restrictions)
  2. Have some students press start and submit, others NOT press start at all
  3. Archive the training day
  4. Navigate to Combined Ranking tab
  5. Verify: students who missed start still have task columns with 0 scores (based on their tags)
  6. Verify: "N/A" shows for tasks the student's tags don't match
  7. Check that students who missed start now have StudentTask records for their visible tasks
  8. Verify those students can submit to those tasks from the training program management contest
  9. Click "[history]" for a student and verify graphs show correct scale and submission times are correct
  10. Apply date filters and verify both table and history view filter correctly

Notes

Summary by CodeRabbit

Release Notes

  • New Features

    • Added ability to archive training days with a confirmation workflow that preserves attendance and ranking data.
    • Added attendance view for training programs with date filtering to display archived records across students and training days.
    • Added combined ranking views to display aggregated and detailed student rankings across archived training days.
  • Enhancements

    • Separated active and archived training days in the UI for better organization.
    • Training days now support metadata fields: name, description, and start time.
    • Improved handling of training days that no longer have active contests.

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

This commit implements the archive training day feature which allows:
- Archiving a training day by extracting attendance and ranking data
- Storing attendance data (status, location, delay time, delay reasons)
- Storing ranking data (tag, task scores, score history)
- Deleting the contest and participations after archiving
- Displaying archived training days separately from active ones
- New Attendance tab to view attendance data for all archived training days

New database models:
- ArchivedAttendance: stores attendance data for archived training days
- ArchivedStudentRanking: stores ranking data for archived training days

Changes to TrainingDay model:
- Added name and description fields (synced with contest while it exists)
- Made contest_id nullable (NULL after archiving)

New handlers:
- ArchiveTrainingDayHandler: handles archiving with IP selection for location
- TrainingProgramAttendanceHandler: displays attendance data with filtering

UI updates:
- Training days page now shows active and archived training days separately
- Archive button added to active training days
- New Attendance tab in training program sidebar
- Archive confirmation page with class IP selection

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

🤖 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 15, 2026

📝 Walkthrough

Walkthrough

Adds archival support: two new ORM models (ArchivedAttendance, ArchivedStudentRanking), TrainingDay fields/relationships to hold archived metadata, admin handlers and templates to perform and view archives/attendance, and a DB migration creating archival tables and adjusting training_days.contest FK.

Changes

Cohort / File(s) Summary
Archived models
cms/db/archived_attendance.py, cms/db/archived_student_ranking.py
New ORM models and tables for immutable archived attendance and student ranking data (composite unique keys on training_day_id+student_id, FK cascades, indexes, JSONB/ARRAY fields for scores/submissions/history).
Model exports & TrainingDay updates
cms/db/__init__.py, cms/db/training_day.py
Exported new archived models; made training_days.contest_id nullable with ON DELETE SET NULL; added name, description, start_time, archived_tasks_data; added relationships archived_attendances and archived_student_rankings.
Archival handlers & logic
cms/server/admin/handlers/trainingprogram.py
Adds ArchiveTrainingDayHandler (GET/POST archival flow), attendance and combined-ranking handlers, helper methods to build ArchivedAttendance/ArchivedStudentRanking records, notification/emission on success/failure, and a small skip-if-no-contest fix in AddTrainingProgramStudentHandler.
Handler exports & routing
cms/server/admin/handlers/__init__.py
Exposes new handlers and registers routes for archive, attendance, combined_ranking, combined_ranking/history, and combined_ranking_detail endpoints.
Admin templates (archive & views)
cms/server/admin/templates/archive_training_day.html, cms/server/admin/templates/training_program_attendance.html, cms/server/admin/templates/training_program_combined_ranking.html, cms/server/admin/templates/training_program_combined_ranking_detail.html, cms/server/admin/templates/training_program_training_days.html, .../training_program_tasks.html, .../student_tasks.html, .../training_program_task_remove.html
New archive confirmation template; attendance, combined-ranking, detail views; UI changes to list active vs archived days; multiple templates updated to safely render training days that lack contests.
Base template & navigation
cms/server/admin/templates/base.html
Sidebar/training program menu now hides training days without contests and adds Attendance / Combined Ranking entries.
Static styles
cms/server/admin/static/aws_style.css
Adds extensive CSS for attendance & ranking views (sticky headers/first column, status badges, responsive table styles).
DB migration
cmscontrib/updaters/update_from_1.5.sql
Creates archived_attendances and archived_student_rankings tables with indexes and constraints; adds training_days columns (name, description, start_time, archived_tasks_data); updates training_days.contest_id FK to ON DELETE SET NULL.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin UI
    participant Handler as ArchiveTrainingDayHandler
    participant DB as Database
    participant Notify as Notification System

    Admin->>Handler: GET archive page (show shared IPs)
    Admin->>Handler: POST archive request
    Handler->>DB: Load TrainingDay & Contest
    Handler->>DB: For each participation -> create ArchivedAttendance
    Handler->>DB: For each student -> create ArchivedStudentRanking
    Handler->>DB: Save TrainingDay archival metadata (name/description/start_time/archived_tasks_data)
    Handler->>DB: Delete Contest (cascades participations/submissions)
    alt success
        Handler->>Notify: emit success
        Notify->>Admin: show confirmation
    else failure
        Handler->>Notify: emit error
        Notify->>Admin: show error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Daniel-Aga
  • Command-Master
  • eden-az

Poem

🐰 I dug a burrow for bygone days,
Stashed scores and names in tidy arrays,
Hopped through templates, handlers, and styles,
Archived each memory across many miles —
A rabbit's cheer for preserved learning ways! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% 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 clearly and accurately summarizes the main purpose of the PR: adding archive training day functionality with combined ranking capabilities.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 15, 2026 10:02
- Changed ArchivedStudentRanking.student_tags from JSONB to ARRAY(Unicode)
- Added GIN index for efficient filtering by tag
- Updated archiving logic to store all tags as a list

Co-Authored-By: Ron Ryvchin <[email protected]>
The SQLAlchemy model uses Python-side default=list, not a database-level
DEFAULT, so the SQL migration should not have a DEFAULT clause.

Co-Authored-By: Ron Ryvchin <[email protected]>
…n.ip

The starting_ip_addresses field stores the actual IPs from which students
participated, while participation.ip is for allowed IP restrictions.

Co-Authored-By: Ron Ryvchin <[email protected]>
- Fix 'None' has no attribute 'id' error on training_program_tasks.html
  by checking if training_day.contest is not none before accessing .id
- Fix attendance archiving bug: lookup student by user_id instead of
  participation_id since Student.participation_id points to managing
  contest participation, not training day participation
- Add start_time field to TrainingDay model to store contest start time
  after archiving
- Display start_time in archived training days table with sorting

Co-Authored-By: Ron Ryvchin <[email protected]>
- Fix ScoreHistory.time -> ScoreHistory.timestamp in archiving code
  (the model uses 'timestamp' not 'time')
- Fix sidebar to filter out archived training days (where contest is None)
  to prevent 'None' has no attribute errors when accessing td.contest

Co-Authored-By: Ron Ryvchin <[email protected]>
- Add colored badges with icons for student statuses (On Time, Delayed, Missed)
- Add colored badges for location (Class, Home, Both)
- Color columns within each training day (status, location, reasons)
- Change training day titles to show description with start date in brackets
- Fix date filtering to actually filter archived training days by start_time
- Order results by start_time instead of position
- Add Clear button to reset date filters
- Fix location to default to 'home' when no class IP is chosen during archiving
- Fix location to default to 'home' when participant has no IP recorded

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

ronryv commented Jan 15, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 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: 1

🤖 Fix all issues with AI agents
In `@cms/server/admin/templates/training_program_attendance.html`:
- Around line 233-239: The template builds the student edit link with the wrong
ID: it uses student.participation.user.id but the route expects the student ID;
update the link generation to pass student.id (i.e. change the url call in the
table header from student.participation.user.id to student.id so the path
url('training_program', training_program.id, 'student', student.id, 'edit')
points to the correct handler).
🧹 Nitpick comments (6)
cms/server/admin/templates/archive_training_day.html (1)

58-66: Consider adding explicit confirmation for this destructive action.

Since archiving permanently deletes the contest and participations, consider adding an additional confirmation mechanism such as requiring the admin to type the contest name or check a confirmation checkbox. This provides an extra safeguard against accidental archiving.

💡 Optional: Add confirmation checkbox
   <h2>Confirm Archive</h2>
   <p>
+    <label>
+      <input type="checkbox" name="confirm_archive" required />
+      I understand this action is irreversible and will permanently delete the contest
+    </label>
+  </p>
+  <p>
     <input type="submit" value="Archive Training Day"
 {% if not admin.permission_all %}
            disabled
 {% endif %}
     />
   </p>
cms/server/admin/templates/training_program_attendance.html (1)

259-269: Consider adding a fallback for missing location.

When att.location is None, no location is displayed. While the archiving handler should always set a value, adding a fallback would improve robustness.

Optional: Add fallback for None location
                 <div class="location-row">
                   {% if att.location == "class" %}
                     <span class="location-icon">&#x1F3EB;</span> Class
                   {% elif att.location == "home" %}
                     <span class="location-icon">&#x1F3E0;</span> Home
                   {% elif att.location == "both" %}
                     <span class="location-icon">&#x21C4;</span> Both
                   {% elif att.location %}
                     <span>{{ att.location }}</span>
+                  {% else %}
+                    <span class="location-icon">&#x2753;</span> Unknown
                   {% endif %}
                 </div>
cms/db/training_day.py (1)

123-126: Type annotation inconsistent with nullable contest_id.

The contest relationship type hint is "Contest" but since contest_id is now nullable, the relationship can be None after archiving. Consider updating the type annotation for consistency.

Suggested fix
-    contest: "Contest" = relationship(
+    contest: "Contest | None" = relationship(
         "Contest",
         back_populates="training_day",
     )
cms/server/admin/handlers/trainingprogram.py (3)

2175-2175: Consider moving import to top of file.

The DelayRequest import is inside the method. While this works, it's more idiomatic to import at the module level for consistency with other imports.

Suggested change at top of file (around line 30-33)
 from cms.db import Contest, TrainingProgram, Participation, Submission, \
     User, Task, Question, Announcement, Student, StudentTask, Team, \
     TrainingDay, TrainingDayGroup, ArchivedAttendance, ArchivedStudentRanking, \
-    ScoreHistory
+    ScoreHistory, DelayRequest

Then remove the import from line 2175.


2228-2228: Consider moving import to top of file.

Similar to DelayRequest, ParticipationTaskScore is imported inside the method. Consider adding it to the module-level imports for consistency.

Suggested change at top of file (around line 30-33)
 from cms.db import Contest, TrainingProgram, Participation, Submission, \
     User, Task, Question, Announcement, Student, StudentTask, Team, \
     TrainingDay, TrainingDayGroup, ArchivedAttendance, ArchivedStudentRanking, \
-    ScoreHistory
+    ScoreHistory, DelayRequest, ParticipationTaskScore

Then remove the import from line 2228.


2305-2306: Use existing datetime import for timedelta.

The timedelta is imported inside the method, but datetime is already imported as dt at the top. You can use dt module's timedelta or import timedelta alongside at module level.

Suggested fix

Change line 24 to include timedelta:

-from datetime import datetime as dt
+from datetime import datetime as dt, timedelta

Then change lines 2305-2306 to:

-            from datetime import timedelta
             query = query.filter(TrainingDay.start_time < end_date + timedelta(days=1))
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6c69f6 and a09184a.

📒 Files selected for processing (12)
  • cms/db/__init__.py
  • cms/db/archived_attendance.py
  • cms/db/archived_student_ranking.py
  • cms/db/training_day.py
  • cms/server/admin/handlers/__init__.py
  • cms/server/admin/handlers/trainingprogram.py
  • cms/server/admin/templates/archive_training_day.html
  • cms/server/admin/templates/base.html
  • cms/server/admin/templates/training_program_attendance.html
  • cms/server/admin/templates/training_program_tasks.html
  • cms/server/admin/templates/training_program_training_days.html
  • 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/trainingprogram.py
  • cms/server/admin/handlers/__init__.py
🧬 Code graph analysis (4)
cms/db/archived_attendance.py (3)
cms/db/base.py (1)
  • Base (67-323)
cms/db/training_day.py (1)
  • TrainingDay (65-156)
cms/db/student.py (1)
  • Student (38-89)
cms/db/__init__.py (2)
cms/db/archived_attendance.py (1)
  • ArchivedAttendance (38-98)
cms/db/archived_student_ranking.py (1)
  • ArchivedStudentRanking (38-97)
cms/db/training_day.py (2)
cms/db/archived_attendance.py (1)
  • ArchivedAttendance (38-98)
cms/db/archived_student_ranking.py (1)
  • ArchivedStudentRanking (38-97)
cms/server/admin/handlers/__init__.py (1)
cms/server/admin/handlers/trainingprogram.py (2)
  • ArchiveTrainingDayHandler (2015-2267)
  • TrainingProgramAttendanceHandler (2270-2341)
🪛 Ruff (0.14.11)
cms/server/admin/handlers/trainingprogram.py

2098-2098: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (25)
cms/server/admin/templates/base.html (2)

313-317: LGTM! Correctly filters out archived training days from sidebar.

The filter if td.contest appropriately excludes archived training days (where contest_id is null) from the sidebar navigation, since they no longer have active contests to link to.


482-482: LGTM! New Attendance navigation entry.

The new menu entry follows the existing pattern and provides navigation to the attendance feature for archived training days.

cms/db/__init__.py (2)

58-58: LGTM! New archived models added to exports.

The new ArchivedAttendance and ArchivedStudentRanking models are correctly added to __all__ alongside related training program models.


114-115: LGTM! Import statements follow existing pattern.

The imports for the new archived data models are consistent with the module's import organization.

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

76-80: LGTM! Properly handles archived training days without contests.

The updated conditional logic correctly handles the case where a training day exists but its contest has been archived (deleted). The fallback to td.name or "(archived)" provides clear indication to users.

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

12-13: LGTM! Clear separation of active and archived training days.

The template correctly uses selectattr("contest") to filter active training days (those with associated contests) and presents them in a dedicated section with the ability to archive.

Also applies to: 48-49


63-65: LGTM! Archive action link added.

The archive link correctly routes to the archive confirmation page for each active training day.


82-112: LGTM! Well-structured archived training days section.

The archived section correctly uses rejectattr("contest") to show only archived days, and provides appropriate fallbacks for missing name/description/start_time fields.


114-118: LGTM! Table sorting enabled for archived days.

The sort initialization for the archived table uses correct column index (2 for Start column in the 3-column table).

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

12-21: LGTM! Clear warning about destructive action.

The warning notification clearly communicates the consequences of archiving, including the permanent deletion of the contest and participations. This is important for preventing accidental data loss.


33-56: LGTM! Well-designed class IP selection interface.

The IP selection table is sorted by student count in descending order (most common IPs first), making it easier for admins to identify classroom IPs. The fallback message when no shared IPs exist is appropriate.

cms/db/archived_attendance.py (1)

38-98: LGTM! Well-structured archival model.

The model correctly implements the archival pattern with:

  • Appropriate FK constraints with CASCADE delete behavior
  • Composite unique constraint preventing duplicate records
  • Proper indexing on foreign key columns
  • Clear separation between required (status) and optional (location, delay_time, delay_reasons) fields
cms/server/admin/handlers/__init__.py (1)

187-189: LGTM! Routes and imports follow established patterns.

The new handlers are properly imported and routed, consistent with the existing training program URL structure.

Also applies to: 361-362

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

568-664: LGTM! Migration is well-structured and complete.

The SQL migration correctly:

  • Adds archival columns to training_days (name, description, start_time)
  • Changes contest_id from CASCADE to SET NULL for proper archival behavior
  • Creates both archival tables with appropriate constraints, indexes (including GIN for tags), and foreign keys matching the ORM models
cms/db/archived_student_ranking.py (1)

38-97: LGTM! Well-designed archival ranking model.

The model correctly implements:

  • JSONB columns for flexible storage of task scores and score history
  • PostgreSQL ARRAY with GIN index for efficient tag-based filtering
  • Consistent structure with ArchivedAttendance model
  • Proper type hints reflecting the expected data structures
cms/db/training_day.py (3)

86-92: LGTM on the nullable contest_id change.

The change to make contest_id nullable with ondelete="SET NULL" correctly supports the archival workflow where the contest is deleted but the training day record persists.


99-116: LGTM on the new archival fields.

The name, description, and start_time fields appropriately preserve contest metadata after archiving. The comments clearly document the sync-while-active / preserve-after-archive behavior.


146-156: LGTM on the archived relationships.

The cascade="all, delete-orphan" configuration ensures archived attendance and ranking data are properly cleaned up when a training day is deleted.

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

32-33: LGTM on the new imports.

The imports for ArchivedAttendance, ArchivedStudentRanking, and ScoreHistory are necessary for the archiving functionality.


449-451: LGTM on skipping archived training days.

Correctly guards against attempting to create participations for archived training days that no longer have an associated contest.


2018-2056: LGTM on the archive confirmation page handler.

The validation checks and IP counting logic are well-implemented. The shared IP detection (filtering to IPs with >1 student) provides useful context for the admin to identify class IPs.


2098-2103: Broad exception handling is consistent with existing patterns.

While static analysis flags except Exception, this follows the established handler pattern in this codebase for gracefully handling various failure modes during complex operations. The notification and redirect behavior is appropriate. Based on learnings, the centralized error handling after try_commit() is relied upon for commit failures.


2114-2196: LGTM on attendance archival logic.

The attendance extraction correctly:

  • Looks up students via Participation.user_id join (since Student references managing contest participation)
  • Handles all location edge cases (no class IPs, no participant IPs, mixed IPs)
  • Defaults to "home" when IP data is unavailable
  • Aggregates delay reasons from DelayRequest records

2198-2267: LGTM on ranking archival logic.

The ranking extraction correctly:

  • Preserves all student tags as an array for filtering
  • Stores task scores keyed by task ID (as string for JSON compatibility)
  • Converts score history timestamps to offsets from contest start in RWS format

2270-2341: LGTM on attendance display handler.

The handler correctly:

  • Filters archived training days by date range
  • Builds a student-to-training-day attendance matrix
  • Orders training days by start_time for consistent display
  • Handles missing participation gracefully in the sort key

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

@ronryv ronryv force-pushed the devin/1768469828-archive-training-day branch from a09184a to 7b6ee72 Compare January 15, 2026 18:13
- Add TrainingProgramCombinedRankingHandler for main page with date filtering
- Add TrainingProgramCombinedRankingHistoryHandler for JSON history endpoint
- Add TrainingProgramCombinedRankingDetailHandler for student detail view
- Create templates for combined ranking and detail pages
- Add navigation link in sidebar
- Support multi-contest format in history view (each training day is a contest)

Co-Authored-By: Ron Ryvchin <[email protected]>
@devin-ai-integration devin-ai-integration bot changed the title Add archive training day feature Add archive training day feature with combined ranking Jan 15, 2026
ronryv and others added 9 commits January 15, 2026 22:08
The archiving code was incorrectly using contest.tasks (which is empty for
training day contests) instead of training_day.tasks. Also changed to use
the task_score function with the managing contest participation to compute
actual scores, matching how the ranking handler works.

Changes:
- Use training_day.tasks instead of contest.tasks
- Get managing contest participation for each user
- Use task_score() to compute scores filtered by training day
- Filter score history to only include tasks in the training day

Co-Authored-By: Ron Ryvchin <[email protected]>
Changed the archiving code to use get_cached_score_entry which:
1. Uses the score cache efficiently instead of recalculating
2. Handles training day participations correctly (queries submissions
   from managing contest filtered by training_day_id)
3. Uses training_day.tasks instead of contest.tasks

Also fixed ScoreHistory query to use training day participation ID
since history is stored with the training day participation.

Co-Authored-By: Ron Ryvchin <[email protected]>
…nd task metadata

- Store task_scores for ALL visible tasks (including 0 scores) to differentiate
  between inaccessible tasks and 0-score tasks
- Add submissions field to ArchivedStudentRanking for rendering submission tables
- Add archived_tasks_data to TrainingDay to preserve scoring scheme at archive time
- Update combined ranking table to show N/A for inaccessible tasks
- Update combined ranking detail page to use archived task metadata and submissions
- Simplify data model: use task_scores keys for visibility instead of separate field

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

When archiving a training day, determine which tasks should be visible to each
student based on their tags (using can_access_task), rather than only including
tasks where the student already has a StudentTask record. This ensures students
who missed the start button still get the training day's tasks in their archive
with 0 scores.

Co-Authored-By: Ron Ryvchin <[email protected]>
When archiving a training day, create StudentTask records for visible tasks
that the student doesn't already have. This allows students who missed the
training to still submit those tasks from home via the training program
management contest, mirroring the behavior of the start button.

Co-Authored-By: Ron Ryvchin <[email protected]>
The contest begin/end times were set to absolute timestamps (td.start_time),
but the history and submission times are stored as offsets from the original
contest start. This caused:
1. Graph scale to be incorrect (data points outside the visible range)
2. Submission times to display incorrectly (negative values)

Fix: Set contest begin to 0 and calculate end from the max history time,
since all times are stored as offsets from contest start.

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

ronryv commented Jan 16, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 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: 2

🤖 Fix all issues with AI agents
In `@cms/server/admin/static/aws_style.css`:
- Around line 1223-1292: The global selectors .page-header and .btn-primary in
aws_style.css are overriding Bootstrap site-wide; scope them by nesting or
renaming—e.g., change selectors to .training-program-view .page-header and
.training-program-view .btn-primary (or rename to .tp-page-header and
.tp-btn-primary) and update any templates that render training program pages to
wrap content in the .training-program-view container so only
training-program-specific headers and primary buttons get the new flex and teal
styles while leaving Bootstrap's defaults elsewhere; keep other classes like
.filter-card/.filter-group/.btn-text unchanged.

In `@cms/server/admin/templates/training_program_combined_ranking.html`:
- Around line 86-91: The empty-state colspan currently uses "{{ 1 +
archived_training_days|length }}" which only covers the date column and archived
training day columns; update it to compute the full number of table columns by
adding the task columns as well (e.g. replace with "{{ 1 +
archived_training_days|length + tasks|length }}" or the correct task-list
variable used in this template) so the "No students found" row spans all task
columns and keeps the table aligned.
🧹 Nitpick comments (2)
cmscontrib/updaters/update_from_1.5.sql (1)

568-572: Consider populating name and description for existing rows.

After adding these nullable columns, existing training_days rows will have NULL values for name and description. If the application code expects these to be populated (even for active training days with contests), you may want to add an UPDATE statement to initialize them from the associated contest data.

Proposed migration addition
-- After adding the columns, populate from existing contest data
UPDATE public.training_days td
SET name = c.name,
    description = c.description
FROM public.contests c
WHERE td.contest_id = c.id
  AND td.name IS NULL;
cms/server/admin/handlers/trainingprogram.py (1)

2385-2413: Consider extracting date filtering logic.

The date parsing and archived training day query pattern is duplicated across TrainingProgramAttendanceHandler, TrainingProgramCombinedRankingHandler, TrainingProgramCombinedRankingHistoryHandler, and TrainingProgramCombinedRankingDetailHandler.

A helper method could reduce duplication:

♻️ Example helper extraction
def _parse_date_range(self) -> tuple[dt | None, dt | None]:
    """Parse start_date and end_date query arguments."""
    start_date = end_date = None
    start_str = self.get_argument("start_date", None)
    end_str = self.get_argument("end_date", None)
    if start_str:
        try:
            start_date = dt.fromisoformat(start_str)
        except ValueError:
            pass
    if end_str:
        try:
            end_date = dt.fromisoformat(end_str)
        except ValueError:
            pass
    return start_date, end_date

def _get_archived_training_days(
    self, training_program_id: int,
    start_date: dt | None, end_date: dt | None
) -> list[TrainingDay]:
    """Query archived training days with optional date filtering."""
    query = (
        self.sql_session.query(TrainingDay)
        .filter(TrainingDay.training_program_id == training_program_id)
        .filter(TrainingDay.contest_id.is_(None))
    )
    if start_date:
        query = query.filter(TrainingDay.start_time >= start_date)
    if end_date:
        query = query.filter(TrainingDay.start_time < end_date + timedelta(days=1))
    return query.order_by(TrainingDay.start_time).all()
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a09184a and 88f6397.

📒 Files selected for processing (14)
  • cms/db/archived_student_ranking.py
  • cms/db/training_day.py
  • cms/server/admin/handlers/__init__.py
  • cms/server/admin/handlers/trainingprogram.py
  • cms/server/admin/static/aws_style.css
  • cms/server/admin/templates/archive_training_day.html
  • cms/server/admin/templates/base.html
  • cms/server/admin/templates/student_tasks.html
  • cms/server/admin/templates/training_program_attendance.html
  • cms/server/admin/templates/training_program_combined_ranking.html
  • cms/server/admin/templates/training_program_combined_ranking_detail.html
  • cms/server/admin/templates/training_program_task_remove.html
  • cms/server/contest/templates/task_submissions.html
  • cmscontrib/updaters/update_from_1.5.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • cms/server/admin/templates/training_program_attendance.html
  • cms/server/admin/templates/archive_training_day.html
🧰 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/trainingprogram.py
  • cms/server/admin/handlers/__init__.py
🧬 Code graph analysis (4)
cms/db/training_day.py (5)
cms/db/base.py (1)
  • Base (67-323)
cms/db/training_program.py (1)
  • TrainingProgram (36-81)
cms/db/training_day_group.py (1)
  • TrainingDayGroup (37-82)
cms/db/archived_attendance.py (1)
  • ArchivedAttendance (38-98)
cms/db/archived_student_ranking.py (1)
  • ArchivedStudentRanking (38-106)
cms/server/admin/handlers/trainingprogram.py (8)
cms/db/contest.py (1)
  • Contest (47-384)
cms/db/training_program.py (1)
  • TrainingProgram (36-81)
cms/db/training_day.py (1)
  • TrainingDay (66-165)
cms/db/archived_attendance.py (1)
  • ArchivedAttendance (38-98)
cms/db/archived_student_ranking.py (1)
  • ArchivedStudentRanking (38-106)
cms/db/scorecache.py (1)
  • ScoreHistory (120-168)
cms/server/util.py (3)
  • get_all_student_tags (64-79)
  • deduplicate_preserving_order (436-451)
  • can_access_task (157-193)
cms/server/contest/handlers/trainingprogram.py (1)
  • get (44-167)
cms/db/archived_student_ranking.py (3)
cms/db/base.py (1)
  • Base (67-323)
cms/db/training_day.py (1)
  • TrainingDay (66-165)
cms/db/student.py (1)
  • Student (38-89)
cms/server/admin/handlers/__init__.py (1)
cms/server/admin/handlers/trainingprogram.py (6)
  • RemoveTrainingDayGroupHandler (1731-1749)
  • ArchiveTrainingDayHandler (2030-2374)
  • TrainingProgramAttendanceHandler (2377-2447)
  • TrainingProgramCombinedRankingHandler (2450-2543)
  • TrainingProgramCombinedRankingHistoryHandler (2546-2595)
  • TrainingProgramCombinedRankingDetailHandler (2598-2776)
🪛 Ruff (0.14.11)
cms/server/admin/handlers/trainingprogram.py

2113-2113: Do not catch blind exception: Exception

(BLE001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (debian:bookworm)
  • GitHub Check: test (ubuntu:noble)
🔇 Additional comments (26)
cms/server/admin/static/aws_style.css (2)

1294-1382: Sticky header + first-column implementation looks solid.

Good use of sticky positioning, z-index layering, and overflow handling to keep headers and student names visible.


1384-1498: Attendance & combined ranking visual styles look good.

The badge system, date/task headers, and score cell differentiation are clear and consistent with the rest of the admin theme.

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

11-14: LGTM!

The additional task.training_day.contest check correctly guards against accessing .contest.description when the training day has been archived (contest_id is NULL). This aligns well with the nullable contest_id schema change.

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

313-317: LGTM!

The filter if td.contest correctly excludes archived training days (where contest_id is NULL) from the sidebar navigation. This prevents displaying broken links and aligns with the schema change making contest_id nullable.


482-483: LGTM!

New navigation entries for Attendance and Combined Ranking tabs are correctly placed within the training program menu section.

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

582-620: LGTM!

The archived_attendances table schema is well-designed:

  • Unique constraint on (training_day_id, student_id) prevents duplicate entries
  • Proper foreign keys with CASCADE delete for referential integrity
  • Appropriate indexes on both foreign key columns

621-661: LGTM!

The archived_student_rankings table is properly structured:

  • GIN index on student_tags array enables efficient tag-based queries
  • JSONB columns for flexible storage of scores, submissions, and history
  • Consistent constraint and index patterns with the attendance table

663-668: LGTM!

Adding start_time and archived_tasks_data columns completes the archive metadata storage requirements for training days.

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

411-411: LGTM!

The fallback chain training_day.contest.description if training_day.contest else (training_day.description or training_day.name) correctly handles archived training days where the contest has been deleted. This ensures a meaningful description is always displayed.

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

57-61: LGTM!

The conditional logic correctly handles archived training days:

  • When contest exists: renders a clickable link to the contest
  • When contest is NULL: displays the training day's description/name with a sensible "Archived Training Day" fallback

This prevents broken links and provides clear context for archived sources.

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

8-22: Filter controls look good.
Clear date inputs + reset link make the flow straightforward for admins.

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

317-412: JS wiring for the archived detail view looks solid.
The override/init flow cleanly adapts the existing renderer to archived data.

cms/db/training_day.py (1)

87-125: Archival metadata fields align with the new workflow.
Persisting name/description/start_time/tasks data after contest deletion is consistent with the archive semantics.

cms/db/archived_student_ranking.py (1)

38-105: Model definition looks consistent with the archival requirements.
Constraints and column structure are coherent for per‑student historical data.

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

364-368: Routes for archive/attendance/combined ranking views are wired cleanly.

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

24-49: LGTM!

Import additions are well-organized and align with the new archiving functionality. The utilities (can_access_task, get_all_student_tags, etc.) and models (ArchivedAttendance, ArchivedStudentRanking, etc.) are appropriately imported for the new handlers.


464-466: LGTM!

Good defensive check to skip archived training days (where contest is None) when adding a new student. This prevents attempting to create participations for contests that no longer exist.


2030-2071: LGTM!

The GET handler correctly validates ownership, checks archive status, and computes shared IPs for class IP selection. The logic to identify IPs with more than one student (count > 1) effectively filters for likely classroom IPs.


2098-2127: Archive operation is well-structured.

The archiving flow correctly:

  1. Preserves contest metadata on the TrainingDay before deletion
  2. Archives attendance and ranking data
  3. Deletes the contest (cascading to participations)

The broad Exception catch at line 2113 (flagged by static analysis) follows the existing codebase pattern for form submission handlers. Based on learnings, this is consistent with how other handlers in this file manage errors.


2129-2210: LGTM!

The attendance archival logic correctly handles:

  • Student lookup via user_id (since Student.participation_id references the managing contest, not training day)
  • Location determination with proper handling of edge cases (no class IPs, no IP recorded, mixed IPs)
  • Delay reason aggregation from DelayRequest records

2212-2298: Well-designed ranking archival logic.

Key aspects are correctly implemented:

  • can_access_task determines per-student task visibility based on tags
  • StudentTask records are created for visible tasks, enabling students who missed training to submit later
  • Task metadata is preserved in archived_tasks_data for historical accuracy

The inline import of get_cached_score_entry at line 2228 is acceptable for avoiding potential circular imports.


2299-2374: LGTM!

The score and submission data collection is well-implemented:

  • Time offsets are calculated consistently from contest.start
  • String keys for task_scores align with JSONB storage patterns
  • Nullable handling (if task_scores else None) avoids storing empty dicts

2377-2447: LGTM!

Attendance display handler correctly:

  • Filters for archived training days (contest_id.is_(None))
  • Applies optional date range filtering
  • Builds a nested dict structure for easy template access
  • Gracefully handles students without participation records in sort key

2546-2595: LGTM!

History endpoint correctly aggregates score history entries across archived training days and returns them in the expected JSON format for graph rendering.


2598-2776: Well-implemented detail view handler.

The handler correctly:

  • Calculates graph scale with begin=0 and end from max history time (fixing the submission time display per PR description)
  • Preserves original scoring scheme via archived_tasks_data
  • Builds comprehensive data structures for the detail template

Note: The Query.get() usage at line 2683 has the same SQLAlchemy 2.0 deprecation concern flagged above.


2512-2520: : The codebase uses SQLAlchemy 1.3.24 (constrained to <1.4), where Query.get() is the standard and correct pattern. Deprecation warnings for Query.get() only apply to SQLAlchemy 2.0+. This pattern is used consistently throughout the codebase, including in utility functions in db/base.py, and requires no changes.

Likely an incorrect or invalid review comment.

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

Comment on lines +1223 to +1292
/* --- Training Program Views (Attendance & Ranking) --- */

/* Layout & Container */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}

.core_title h1 {
margin: 0;
font-size: 1.75rem;
color: #111827;
}

/* Filter Bar */
.filter-card {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}

.filter-group {
display: flex;
align-items: center;
gap: 8px;
}

.filter-label {
font-weight: 600;
font-size: 0.85rem;
color: #4b5563;
}

.filter-input {
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 0.9rem;
}

.btn-primary {
background-color: #0F766E;
color: white;
border: none;
padding: 7px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}

.btn-primary:hover { background-color: #0d655e; }

.btn-text {
color: #6b7280;
text-decoration: underline;
font-size: 0.85rem;
margin-left: auto;
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for usage of the generic class names in HTML templates and views
find . -type f \( -name "*.html" -o -name "*.jinja" -o -name "*.jinja2" \) | head -20

Repository: ioi-isr/cms

Length of output: 166


🏁 Script executed:

# Check usage of the identified generic class names across the entire repo
rg -n '\b(page-header|filter-card|filter-group|btn-primary|btn-text|table-container)\b' --type html --type css

Repository: ioi-isr/cms

Length of output: 19125


🏁 Script executed:

# Check the surrounding CSS context to see if there's already a scoping container
sed -n '1200,1500p' cms/server/admin/static/aws_style.css | head -100

Repository: ioi-isr/cms

Length of output: 1940


Scope these generic selectors to training program views—they override existing Bootstrap styles used site-wide.

The classes .page-header and .btn-primary are already defined in Bootstrap CSS and used across contest templates. Adding unscoped versions in the global admin stylesheet will override those styles throughout the system:

  • .page-header: Used in 12+ contest templates (task submissions, communication, printing, etc.). The new flexbox definition differs from Bootstrap's border-bottom style.
  • .btn-primary: Used 40+ times across admin and contest. The new teal color (#0F766E) overrides Bootstrap's blue (#0088cc).

This will unintentionally change appearance of buttons and headers across the entire contest interface. Scope these selectors under a training-program-specific container (e.g., .training-program-view) or rename with a prefix. The other classes (.filter-card, .filter-group, .btn-text, .table-container) appear only in training program templates and are safe.

Example scoping pattern
- .page-header {
+ .training-program-view .page-header {
     display: flex;
     justify-content: space-between;
     align-items: center;
     margin-bottom: 20px;
     flex-wrap: wrap;
     gap: 15px;
   }

- .btn-primary {
+ .training-program-view .btn-primary {
     background-color: `#0F766E`;
     color: white;
     border: none;
     padding: 7px 16px;
     border-radius: 4px;
     cursor: pointer;
     font-weight: 500;
     transition: background 0.2s;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* --- Training Program Views (Attendance & Ranking) --- */
/* Layout & Container */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.core_title h1 {
margin: 0;
font-size: 1.75rem;
color: #111827;
}
/* Filter Bar */
.filter-card {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-weight: 600;
font-size: 0.85rem;
color: #4b5563;
}
.filter-input {
padding: 6px 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 0.9rem;
}
.btn-primary {
background-color: #0F766E;
color: white;
border: none;
padding: 7px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}
.btn-primary:hover { background-color: #0d655e; }
.btn-text {
color: #6b7280;
text-decoration: underline;
font-size: 0.85rem;
margin-left: auto;
}
/* --- Training Program Views (Attendance & Ranking) --- */
/* Layout & Container */
.training-program-view .page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 15px;
}
.core_title h1 {
margin: 0;
font-size: 1.75rem;
color: `#111827`;
}
/* Filter Bar */
.filter-card {
background-color: `#f9fafb`;
border: 1px solid `#e5e7eb`;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-weight: 600;
font-size: 0.85rem;
color: `#4b5563`;
}
.filter-input {
padding: 6px 10px;
border: 1px solid `#d1d5db`;
border-radius: 4px;
font-size: 0.9rem;
}
.training-program-view .btn-primary {
background-color: `#0F766E`;
color: white;
border: none;
padding: 7px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s;
}
.training-program-view .btn-primary:hover { background-color: `#0d655e`; }
.btn-text {
color: `#6b7280`;
text-decoration: underline;
font-size: 0.85rem;
margin-left: auto;
}
🤖 Prompt for AI Agents
In `@cms/server/admin/static/aws_style.css` around lines 1223 - 1292, The global
selectors .page-header and .btn-primary in aws_style.css are overriding
Bootstrap site-wide; scope them by nesting or renaming—e.g., change selectors to
.training-program-view .page-header and .training-program-view .btn-primary (or
rename to .tp-page-header and .tp-btn-primary) and update any templates that
render training program pages to wrap content in the .training-program-view
container so only training-program-specific headers and primary buttons get the
new flex and teal styles while leaving Bootstrap's defaults elsewhere; keep
other classes like .filter-card/.filter-group/.btn-text unchanged.

Comment on lines +86 to +91
{% else %}
<tr>
<td colspan="{{ 1 + archived_training_days|length }}" style="text-align:center; padding: 20px; color: #6b7280;">
No students found in this training program.
</td>
</tr>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix empty-state colspan to span all task columns.
The colspan only accounts for training days, so the row won’t cover per‑task columns and the table looks misaligned when tasks exist. Consider computing the full column count.

🐛 Proposed fix
{% else %}
+      {% set total_cols = namespace(value=1) %}
+      {% for td in archived_training_days %}
+        {% set total_cols.value = total_cols.value + (training_day_tasks.get(td.id, [])|length) + 1 %}
+      {% endfor %}
       <tr>
-        <td colspan="{{ 1 + archived_training_days|length }}" style="text-align:center; padding: 20px; color: `#6b7280`;">
+        <td colspan="{{ total_cols.value }}" style="text-align:center; padding: 20px; color: `#6b7280`;">
           No students found in this training program.
         </td>
       </tr>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{% else %}
<tr>
<td colspan="{{ 1 + archived_training_days|length }}" style="text-align:center; padding: 20px; color: #6b7280;">
No students found in this training program.
</td>
</tr>
{% else %}
{% set total_cols = namespace(value=1) %}
{% for td in archived_training_days %}
{% set total_cols.value = total_cols.value + (training_day_tasks.get(td.id, [])|length) + 1 %}
{% endfor %}
<tr>
<td colspan="{{ total_cols.value }}" style="text-align:center; padding: 20px; color: `#6b7280`;">
No students found in this training program.
</td>
</tr>
🤖 Prompt for AI Agents
In `@cms/server/admin/templates/training_program_combined_ranking.html` around
lines 86 - 91, The empty-state colspan currently uses "{{ 1 +
archived_training_days|length }}" which only covers the date column and archived
training day columns; update it to compute the full number of table columns by
adding the task columns as well (e.g. replace with "{{ 1 +
archived_training_days|length + tasks|length }}" or the correct task-list
variable used in this template) so the "No students found" row spans all task
columns and keeps the table aligned.

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