Skip to content

Conversation

@ronryv
Copy link
Collaborator

@ronryv ronryv commented Jan 22, 2026

Summary

This PR adds comprehensive training program functionality to CMS, enabling structured training activities with organized participant groups, scheduled training days, student management, attendance tracking, and combined rankings.

Key Features:

  • Training Programs: Create and manage structured training activities with organized participant groups
  • Training Days: Schedule individual training sessions within programs with customizable groups and timing
  • Student Management: Add participants to programs, assign tasks, and manage student tags for categorization
  • Attendance Tracking: Record attendance with status, location, delays, and justification
  • Targeted Announcements: Direct announcements to specific student groups within training
  • Combined Rankings: View aggregated performance across multiple training sessions
  • Excel Export: Download attendance and ranking reports in Excel format

Updates since last revision:

  • Added setup_contest_or_training_program helper method to BaseHandler to reduce code duplication
  • Consolidated TrainingProgramSubmissionsHandler, TrainingProgramAnnouncementsHandler, and TrainingProgramQuestionsHandler with their contest counterparts using (contest|training_program) URL patterns
  • Fixed build_user_to_student_map usage in trainingprogramtask.py to use the utility function instead of inline query
  • Removed duplicate handler classes and cleaned up unused imports

Review & Testing Checklist for Human

  • Database migration (v48→v49): Review cms/db/scripts/db_v49.sql for correctness. Verify all new tables, constraints, and indexes are properly defined
  • Handler consolidation: Test that both /contest/{id}/submissions and /training_program/{id}/submissions routes work correctly (same for announcements and questions)
  • setup_contest_or_training_program helper: Verify the helper correctly sets self.contest and self.r_params for both entity types, and returns the training_program when applicable
  • Training day task visibility: Confirm contest.get_tasks() returns training day tasks (not managing contest tasks) when accessed from a training day contest
  • End-to-end test: Create a training program, add students, create training days with tasks, verify attendance tracking and ranking display work correctly

Notes

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

Summary by CodeRabbit

  • New Features

    • Added training program management for organizing structured training sessions
    • Student enrollment and task assignment within programs
    • Training day archiving with historical attendance and ranking data preservation
    • Tag-based student visibility controls for tasks and announcements
    • Excel export for attendance and combined rankings
    • Score distribution histograms with tag-based filtering
  • Enhancements

    • Delay request management with per-group time windows
    • Combined ranking analytics across training days
    • Task submission tracking by source (training day vs. archive)

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch training_program

Important

Action Needed: IP Allowlist Update

If your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:

  • 136.113.208.247/32 (new)
  • 34.170.211.100/32
  • 35.222.179.152/32

Failure to add the new IP will result in interrupted reviews.


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.

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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
cms/db/submission.py (1)

40-101: ⚠️ Potential issue | 🔴 Critical

Create update_49.sql to add the training_day_id column and index to the submissions table.

The ORM model in cms/db/submission.py adds training_day_id as a nullable foreign key column with an index, but there is no corresponding SQL migration file. Fresh database installations will get this column via metadata.create_all(), but existing databases upgrading through the migration system will lack it. Create cmscontrib/updaters/update_49.sql with an ALTER TABLE statement to add both the column and index to maintain schema consistency across all deployment paths.

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

109-132: ⚠️ Potential issue | 🟠 Major

Preserve visibility tags when editing announcements.

The edit input isn’t pre-populated, so submitting without re-adding tags clears visible_to_tags (and may unintentionally broaden visibility). Populate the input with the current tag list (or ensure your edit-toggle JS does so before submit).

🔧 Proposed fix
-                <input type="text" id="edit_visible_to_tags_{{ msg.id }}" name="visible_to_tags" class="announcement-visibility-tags" style="width: 100%">
+                <input type="text" id="edit_visible_to_tags_{{ msg.id }}" name="visible_to_tags" class="announcement-visibility-tags" style="width: 100%" value="{{ msg.visible_to_tags|join(', ')|e }}">
cms/server/admin/templates/base.html (1)

55-73: ⚠️ Potential issue | 🔴 Critical

Move jQuery before aws_table_utils.js and aws_form_utils.js.
Both files execute jQuery code at load time (e.g., $(selector) on lines 26–27 in aws_table_utils.js and line 30 in aws_form_utils.js), but are currently loaded before jQuery. This causes immediate runtime errors. Load jquery-3.6.0.min.js before these two scripts.

🤖 Fix all issues with AI agents
In `@cms/db/base.py`:
- Around line 276-280: The callable default invocation in cms/db/base.py
currently always calls col.default.arg(None) when col.default.is_callable, which
fails for zero-arg callables (e.g., the password lambda); change the logic in
the block handling col.default (used when fill_with_defaults=True) to detect
callable arity (e.g., via inspect.signature or try/except) and call
col.default.arg() when it accepts zero parameters, otherwise call
col.default.arg(None); update the branch around col.default.is_callable and use
prp.key to set the attribute after selecting the correct call form.

In `@cms/server/admin/handlers/contesttask.py`:
- Around line 119-194: The block guarded by training_day is not None performs
reordering (handlers using REMOVE_FROM_TRAINING_DAY, MOVE_UP, MOVE_DOWN,
MOVE_TOP, MOVE_BOTTOM) without verifying the passed task actually belongs to
that training_day; add an explicit ownership check at the start of that branch
(e.g. confirm task.training_day == training_day or task.training_day_id ==
training_day.id) and raise/return an error or no-op if it doesn't match, so
MOVE_UP/MOVE_DOWN/MOVE_TOP/MOVE_BOTTOM and the REMOVE_FROM_TRAINING_DAY path
only operate on tasks that belong to the specified training_day; reference the
existing symbols task, training_day, REMOVE_FROM_TRAINING_DAY, MOVE_UP,
MOVE_DOWN, MOVE_TOP, MOVE_BOTTOM and use the same sql_session/Task query flow
after the validation.

In `@cms/server/admin/handlers/trainingday.py`:
- Around line 440-446: In the get method, remove the unused local variable
managing_contest assigned from training_program.managing_contest; update the
get(self, training_program_id: str, training_day_id: str) function to not create
managing_contest (leave training_program = self.safe_get_item(TrainingProgram,
training_program_id) and training_day = self.safe_get_item(TrainingDay,
training_day_id) as-is) so there is no unused assignment; ensure no other
references to managing_contest remain in this method.

In `@cms/server/admin/handlers/user.py`:
- Around line 41-42: There are duplicate imports of exclude_internal_contests
(imported both with validate_date_of_birth and again on its own); remove the
redundant import line and ensure exclude_internal_contests and
validate_date_of_birth are imported only once (keep the combined import
statement that includes validate_date_of_birth and exclude_internal_contests and
delete the standalone exclude_internal_contests import) so the module no longer
triggers the F811 duplicate-import error.

In `@cms/server/admin/static/aws_style.css`:
- Around line 1-3: The CSS file begins with a BOM (U+FEFF) immediately before
the leading "html {" selector which causes parsing/analysis errors; remove the
invisible BOM and re-save the file using UTF-8 without BOM so the first
character is the plain "h" of "html {"; you can strip the BOM in your editor or
via a file-encoding conversion tool and verify the file now starts directly with
the "html {" token.

In `@cms/server/admin/templates/fragments/user_detail_scripts.html`:
- Around line 72-80: The AJAX error handler for fetchSubmissionsViaAjax
currently logs and updates UserDetail.submission_table but does not advance
UserDetail.data_fetched or trigger UserDetail.do_show(), which blocks rendering;
update the error function in the $.ajax call (where success uses
UserDetail.submissions_callback) to treat the failure as a completed fetch by
incrementing UserDetail.data_fetched and calling UserDetail.do_show() after
updating the DOM so the rest of the page (navigator) renders.

In `@cms/server/admin/templates/student_tasks.html`:
- Around line 48-53: The label's for attribute ("start_date") doesn't match the
select's id ("task_selector"), breaking the association; update the label's for
to "task_selector" (or alternatively change the select id to "start_date") so
the <label> targets the <select> used in the manual task assignment form (look
for the label with class "tp-filter-label" and the select element with id
"task_selector").
🧹 Nitpick comments (24)
cms/server/admin/static/js/admin_table_utils.js (2)

60-70: Drag cancel (Escape key) still triggers saveNewOrder.

When a user cancels a drag operation (e.g., pressing Escape), the dragend event still fires and calls saveNewOrder(). While the order comparison at lines 108-113 prevents unnecessary saves when the order hasn't changed, if the user moved rows around before pressing Escape, the visual order may have changed from placeholder insertions, potentially triggering an unintended save.

Consider tracking whether a successful drop event occurred before calling saveNewOrder:

♻️ Proposed fix to track drop completion
     var draggedRow = null;
     var placeholder = null;
     var originalOrder = null;
+    var dropCompleted = false;
 
     // ... in drop handler:
     tbody.addEventListener('drop', function(e) {
         e.preventDefault();
         if (placeholder && placeholder.parentNode && draggedRow) {
             placeholder.parentNode.insertBefore(draggedRow, placeholder);
             placeholder.parentNode.removeChild(placeholder);
+            dropCompleted = true;
         }
     });
 
     // ... in dragend handler:
     tbody.addEventListener('dragend', function(e) {
         if (draggedRow) {
             draggedRow.classList.remove('dragging');
             if (placeholder && placeholder.parentNode) {
                 placeholder.parentNode.removeChild(placeholder);
             }
+            if (dropCompleted) {
+                saveNewOrder();
+            } else {
+                restoreOriginalOrder();
+            }
             draggedRow = null;
             placeholder = null;
-            saveNewOrder();
+            dropCompleted = false;
         }
     });

157-173: Consider storing Tagify instances for cleanup.

The Tagify instances created here are not stored, which means:

  1. They cannot be destroyed if the filter needs to be reinitialized
  2. Calling initFilterTagify multiple times on the same elements may create duplicate instances (though Tagify may handle this internally)

If reinitialization is not a concern for this use case, this is fine as-is.

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

21-21: Anchor tag missing href attribute affects accessibility.

The "Yes, remove" link lacks an href attribute, which can prevent keyboard users from focusing and activating it in some browsers. Screen readers may also not properly announce it as interactive.

♻️ Proposed fix
-<a onclick="CMS.AWSUtils.ajax_delete('{{ url("training_program", training_program.id, "student", user.id, "remove") }}');" class="button-link">Yes, remove</a>
+<a href="#" onclick="CMS.AWSUtils.ajax_delete('{{ url("training_program", training_program.id, "student", user.id, "remove") }}'); return false;" class="button-link">Yes, remove</a>
cms/server/admin/templates/student_tasks.html (1)

49-49: Unnecessary enctype="multipart/form-data" on form without file upload.

This form only submits a select value. The multipart encoding is typically used for file uploads and adds overhead. Consider removing it.

♻️ Proposed fix
-        <form enctype="multipart/form-data" action="{{ url("training_program", training_program.id, "student", selected_user.id, "tasks", "add") }}" method="POST" style="display: flex; gap: 12px;">
+        <form action="{{ url("training_program", training_program.id, "student", selected_user.id, "tasks", "add") }}" method="POST" style="display: flex; gap: 12px;">
cms/server/admin/static/aws_form_utils.js (1)

271-304: Consider documenting the 200ms timing assumption.

The userRemovalTriggeredAt timing detection relies on a 200ms window to distinguish user-initiated removals from automatic ones (e.g., duplicate detection). This works but is inherently timing-dependent.

Consider adding a comment explaining why 200ms was chosen and under what conditions it might need adjustment.

cms/server/admin/static/aws_table_utils.js (1)

103-109: Double-sort uses same numeric setting for both columns.

The sort applies getRowComparator twice: first for initial_column_idx, then for data_column_idx. Both use the same numeric flag derived from the clicked column's settings. If the initial sort column has a different data type (e.g., string names vs. numeric scores), this could produce unexpected ordering for the secondary sort.

This is likely acceptable if the initial column is typically the same type, but worth noting.

cms/server/admin/templates/macro/delay_request.html (1)

33-38: Consider extracting inline styles to a CSS class.

The warning block uses inline styles for background, border, padding, margin, and border-radius. While functional, extracting these to a reusable CSS class (e.g., .warning-box) would improve maintainability and ensure consistent styling across the admin interface.

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

1-4: Consider reordering template directives.

The {% import %} statement before {% extends %} is unconventional. While Jinja2 processes inheritance correctly regardless of order, the idiomatic pattern places {% extends %} first.

♻️ Suggested reordering
-{% import "macro/reevaluation_buttons.html" as macro_reevaluation_buttons %}
-
 {% extends "base.html" %}
+{% import "macro/reevaluation_buttons.html" as macro_reevaluation_buttons %}
 {% import 'macro/submission.html' as macro_submission %}
cms/server/admin/templates/training_program.html (1)

39-43: Consider adding confirmation for the destructive "Remove" action.

The Remove button directly submits the form without user confirmation. For destructive operations, a confirmation dialog helps prevent accidental data loss.

♻️ Suggested confirmation
 <form action="{{ url("training_programs") }}" method="POST" style="display:inline;">
   {{ xsrf_form_html|safe }}
   <input type="hidden" name="training_program_id" value="{{ training_program.id }}"/>
-  <input type="submit" name="operation" value="Remove" style="float: right;"/>
+  <input type="submit" name="operation" value="Remove" style="float: right;"
+         onclick="return confirm('Are you sure you want to remove this training program? This action cannot be undone.');"/>
 </form>
cms/server/admin/templates/fragments/user_detail_layout.html (1)

12-19: Consider moving inline close-button styles into the shared stylesheet.

Keeps presentation centralized and easier to override.

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

723-734: Use exception chaining for better traceability.

When re-raising an exception as a different type, use raise ... from None to suppress the original exception context or raise ... from err to chain it. This improves stack traces and debugging.

♻️ Proposed fix
         try:
             task.set_default_output_only_submission_format()
-        except Exception:
+        except Exception as err:
             logger.error(
                 "Couldn't create default submission format for task %s "
                 "(dataset %s, type %s)",
                 task.id,
                 task.active_dataset.id,
                 task.active_dataset.task_type,
                 exc_info=True
             )
             raise tornado.web.HTTPError(
                 500, f"Couldn't create default submission format for task {task.id}"
-            )
+            ) from err
cms/server/admin/templates/fragments/training_day_groups.html (1)

78-91: Inconsistent form field naming for alphabetical_task_order.

The checkbox uses alphabetical_{{ group.id }} naming while other fields use array notation (group_id[], start_time[], duration_hours[]). This requires the server handler to use different parsing logic for this field. Consider using consistent array notation or ensure the handler correctly processes the mixed naming patterns.

♻️ Suggestion for consistent naming
       <td style="text-align: center;">
-        <input type="checkbox" name="alphabetical_{{ group.id }}" {{ "checked" if group.alphabetical_task_order else "" }}/>
+        <input type="hidden" name="alphabetical_task_order[]" value="false"/>
+        <input type="checkbox" name="alphabetical_task_order[]" value="true" {{ "checked" if group.alphabetical_task_order else "" }}/>
       </td>

Note: The hidden input ensures a value is always submitted even when unchecked. The server would need to handle pairs of values per group.

cms/db/student_task.py (1)

78-82: Consider adding a default for assigned_at.

The assigned_at column is non-nullable but has no default value. If a StudentTask is created without explicitly providing assigned_at, it will fail. Consider adding a default using datetime.utcnow or func.now() to match common patterns:

Proposed fix
+from sqlalchemy.sql import func
+
 # When the task was assigned to the student.
-assigned_at: datetime = Column(
+assigned_at: datetime = Column(
     DateTime,
     nullable=False,
+    default=func.now(),
 )
cms/db/training_day.py (1)

40-64: Consider defensive check for missing training_program.

The get_managing_participation function accesses training_day.training_program.managing_contest without checking if training_program is None. While the FK constraint makes this unlikely in normal operation, a defensive check could prevent cryptic AttributeError if called with inconsistent data.

Proposed defensive check
 def get_managing_participation(
     session: Session,
     training_day: "TrainingDay",
     user: "User",
 ) -> "Participation | None":
     ...
     from . import Participation
+    if training_day.training_program is None:
+        return None
     managing_contest = training_day.training_program.managing_contest
     return (
         session.query(Participation)
         .filter(Participation.contest_id == managing_contest.id)
         .filter(Participation.user_id == user.id)
         .first()
     )
cms/server/admin/static/aws_tp_styles.css (1)

966-977: Consolidate duplicate .tp-btn-secondary definitions.
The class is defined twice with slightly different properties; merging reduces style drift.

Also applies to: 2464-2485

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

145-156: Avoid double-scanning students for main group.
check_training_day_eligibility already walks the roster and can return the main group. Calling get_participation_main_group again repeats that scan and can drift if logic diverges. Consider reusing the main_group from the eligibility call (and caching it for warnings/CSV) to reduce per-request cost.

♻️ Suggested tweak
-        for participation in participations:
-            # For training day contests, check eligibility and skip ineligible students
-            if self.contest.training_day is not None:
-                is_eligible, _, _ = check_training_day_eligibility(
-                    self.sql_session, participation, self.contest.training_day
-                )
+        for participation in participations:
+            main_group = None
+            if self.contest.training_day is not None:
+                is_eligible, main_group, _ = check_training_day_eligibility(
+                    self.sql_session, participation, self.contest.training_day
+                )
                 if not is_eligible:
                     continue
-
-            main_group = get_participation_main_group(self.contest, participation)
cms/db/task.py (1)

64-121: Consider enforcing training_day_num when training_day_id is set.
Postgres treats NULL as distinct in unique constraints, so multiple tasks assigned to the same training day can still share a NULL order. If every assigned task must have a position, add a check constraint (or enforce at the model layer) to prevent training_day_id with training_day_num IS NULL.

🔧 Possible constraint
 __table_args__ = (
     UniqueConstraint('contest_id', 'num'),
     UniqueConstraint('contest_id', 'name'),
     UniqueConstraint('training_day_id', 'training_day_num'),
+    CheckConstraint("training_day_id IS NULL OR training_day_num IS NOT NULL"),
     ForeignKeyConstraint(
cms/server/admin/handlers/contest.py (1)

235-253: Consider using a custom exception class for validation errors.

The static analysis hints suggest abstracting these ValueError raises to avoid long messages in raises. While this is a minor style concern, the validation logic itself is correct - ensuring training day times don't conflict with main group times.

♻️ Optional: Extract validation to helper
+def _validate_training_day_times(training_day, new_start, new_stop):
+    """Validate training day times against main group times."""
+    for group in training_day.groups:
+        if group.start_time is not None and new_start is not None:
+            if new_start > group.start_time:
+                raise ValueError(
+                    f"Training day start cannot be after main group "
+                    f"'{group.tag_name}' start time"
+                )
+        if group.end_time is not None and new_stop is not None:
+            if new_stop < group.end_time:
+                raise ValueError(
+                    f"Training day end cannot be before main group "
+                    f"'{group.tag_name}' end time"
+                )
+
 # In ContestHandler.post():
-            for group in training_day.groups:
-                if group.start_time is not None and new_start is not None:
-                    # ... validation logic ...
+            _validate_training_day_times(training_day, new_start, new_stop)
cms/server/admin/templates/training_program_attendance.html (2)

69-75: Export URL construction could be simplified with urlencode.

The manual URL construction with conditional ampersands is error-prone. Consider using a template helper or building the query string programmatically.


135-135: Consider using data attributes instead of inline onclick.

The onclick="openAttendanceModal(this)" works but separating JavaScript from HTML is generally preferred for maintainability. However, this is a minor stylistic point.

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

333-442: Consider batching task-archive progress calculation.
calculate_task_archive_progress(...) runs once per participation, which can lead to N+1 StudentTask queries on large programs. Prefetch StudentTask rows (and any cached scores) in bulk and compute progress in-memory, or extend the helper to accept preloaded data.

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

167-266: Batch user/participation lookups to avoid N+1 queries.
The per-username user lookup and per-user participation check can be batched to reduce DB round-trips on large files.

♻️ Proposed refactor (batch lookups)
-            for username in usernames:
-                user = self.sql_session.query(User).filter(
-                    User.username == username).first()
+            users = (
+                self.sql_session.query(User)
+                .filter(User.username.in_(usernames))
+                .all()
+            )
+            user_by_username = {u.username: u for u in users}
+            existing_user_ids = {
+                uid for (uid,) in self.sql_session.query(Participation.user_id)
+                .filter(Participation.contest == managing_contest)
+                .filter(Participation.user_id.in_([u.id for u in users]))
+                .all()
+            }
+
+            for username in usernames:
+                user = user_by_username.get(username)
@@
-                    existing_participation = (
-                        self.sql_session.query(Participation)
-                        .filter(Participation.contest == managing_contest)
-                        .filter(Participation.user == user)
-                        .first()
-                    )
-
-                    if existing_participation is not None:
+                    if user.id in existing_user_ids:
cms/server/admin/handlers/base.py (2)

355-414: Consider narrowing the exception handling scope.

The broad except Exception: pass at line 413-414 could silently hide unexpected errors. While the intent is to gracefully fall back to normal request handling if the redirect logic fails, catching all exceptions makes debugging difficult.

♻️ Proposed refinement
-            except Exception:
-                pass
+            except (ValueError, AttributeError, KeyError):
+                # Redirect logic failed - continue with normal request handling
+                pass

991-994: Use exception chaining for cleaner stack traces.

The static analysis correctly identifies that the exception should be raised with explicit chaining to clarify the relationship between the ValueError and the HTTPError.

♻️ Proposed fix
         try:
             user_id_int = int(user_id)
         except ValueError:
-            raise tornado.web.HTTPError(404)
+            raise tornado.web.HTTPError(404) from None

@ronryv ronryv force-pushed the training_program branch 2 times, most recently from eefcb58 to 55409d2 Compare February 2, 2026 07:48
devin-ai-integration bot and others added 24 commits February 2, 2026 21:59
…ents (#52)

* Add Training Programs feature (Phase 1)

This commit introduces the Training Programs feature for organizing
year-long training with multiple weekly contests.

Phase 1 includes:
- TrainingProgram database model with managing contest relationship
- Database migrations (version 47 -> 48)
- Admin CRUD handlers and templates for training programs
- Training Programs navigation entry in admin sidebar
- Basic CWS training program overview page with score tracking

The training program creates a managing contest (prefixed with __) that
handles all submissions and user participations for the program.

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

* Fix CI failures: add training_program to import spec and fix schema

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

* Improve training program overview page with filtering, sorting, and progress bar

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

* Hide system users/contests and prevent manual creation with __ prefix (#41)

* Hide system users and contests from admin lists and prevent manual creation

- Filter out users/contests starting with '__' from admin list views
- Add validation to prevent creating users with '__' prefix in:
  - Registration handler (contest web server)
  - Admin user creation handler
  - Admin user edit handler
- Add validation to prevent creating contests with '__' prefix in:
  - Admin contest creation handler
  - Admin contest edit handler
- Fix line length issues to comply with flake8

This ensures that system entities like __model_solutions__ user and
__model_solutions_system__ contest remain hidden from admins and cannot
be accidentally created or modified through the UI.

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

* Fix: Hide system users/contests from unassigned dropdowns

- Filter unassigned_users in ContestUsersHandler to exclude users starting with '__'
- Filter unassigned_contests in UserHandler to exclude contests starting with '__'
- Use proper SQL LIKE with escape to avoid wildcard issues

This fixes the issue where __model_solutions__ user was visible in the
'Add a new user' dropdown when adding users to a contest, and ensures
__model_solutions_system__ contest doesn't appear when adding participations.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Fix XSRF error on training program delete by using CMS.AWSUtils.ajax_delete

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

* Merge remote changes and add training program URL routing, browse page, and default overview redirect

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

* Add Student model and training program admin interface improvements

- Create Student model with student_tags (list of strings) for training program participation
- Add SQL migration for students table in update_from_1.5.sql
- Update sidebar in base.html to show training programs list like contests
- Add training program detail page with tabs: General, Ranking, Submissions, Students, Tasks, Announcements, Questions
- Create handlers for student management that sync with managing contest's Participation
- Create templates for students and tasks pages
- Register all new routes in handlers/__init__.py

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

* Fix SQL migration schema mismatch for students.participation_id

Use a UNIQUE INDEX instead of separate UNIQUE constraint and regular index
to match SQLAlchemy's schema generation.

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

* Fix 4 bugs in training program admin interface

1. Fix student tags error by restructuring template to avoid nested forms
2. Fix announcement redirect by checking for training_program context in template
3. Fix ranking page crash by duplicating ranking logic from RankingHandler
4. Fix side menu visibility by updating base.html conditional

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

* Add student detail page and redirect managing contest URLs to training programs

- Create ManagingContestRedirectHandler to redirect /contest/{managing_contest_id}/* URLs to /training_program/{id}/*
- Create StudentHandler to show and edit student details including tags
- Create student.html template based on participation.html with student tags field
- Update training_program_students.html to link to student page instead of participation page
- Remove UpdateStudentTagsHandler as it's now redundant (tags edited on student page)
- Remove 'Update Student Tags' section from students list

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

* Fix CI: Move managing contest redirect logic from catch-all route to BaseHandler.prepare()

The ManagingContestRedirectHandler was intercepting all /contest/{id}/* URLs
and breaking normal contest functionality by raising 404 for non-managing
contests. This caused the folder_functional_test to fail.

The fix moves the redirect logic to BaseHandler.prepare() which is called
before any request handler method. This allows normal contest handlers to
work while still redirecting managing contest URLs to training program URLs.

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

* Fix 403 error when accessing training program tasks

Skip the starting_time check for training programs since they are
meant to be always accessible (year-long training, not time-limited
contests). Normal contests still require users to 'start' before
accessing tasks.

Affected handlers:
- TaskDescriptionHandler
- TaskStatementViewHandler
- TaskAttachmentViewHandler
- SubmitHandler
- TaskSubmissionsHandler
- SubmissionStatusHandler
- SubmissionDetailsHandler
- SubmissionFileHandler
- UseTokenHandler

Co-Authored-By: [email protected] <[email protected]>

* Fix training program removal: add POST handler to redirect to confirmation page

Co-Authored-By: [email protected] <[email protected]>

* Add navigation links between task description and submissions pages

- Add Submit column with link to submissions page in training program task archive
- Add 'Go to submissions' button at bottom of task description pages
- Add 'Go to statement' button at top of task submissions pages

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

* Fix training program removal: add missing 'contest' and 'unanswered' variables for base template

Co-Authored-By: [email protected] <[email protected]>

* Fix Overview/Resource usage 404 and Questions link on training program pages

- Use global Overview/Resource usage links when training_program is defined
- Add unanswered variable to TrainingProgramHandler for Questions link count

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

* Remove managing contest links and link users to student pages on General page

- Remove Managing Contest row from the form table
- Remove Managing Contest Details section with instructions
- Change Users section to Students section
- Link student names to student detail page instead of user page

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

* Fix sidebar for tasks/submissions in training programs

When viewing a task or submission that belongs to a training program's
managing contest, automatically set training_program in render_params
so the sidebar shows the training program menu instead of the managing
contest menu.

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

* Fix error 405 when removing student from training program

The template was using a POST form but the handler only has a DELETE
method. Changed to use ajax_delete like other remove templates.

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

* Fix NotNullViolation when removing student from training program

Delete the Student record before deleting the Participation to avoid
the NOT NULL constraint violation on students.participation_id.

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

* Fix question replies and announcement editing/removing in training programs

- Fix 404 when replying to questions: The prepare() method was redirecting
  /contest/{id}/question/... URLs to training program URLs which don't exist.
  Added exception for question and announcement paths to allow them to use
  the contest handlers directly.

- Fix question action redirects: After replying/ignoring/claiming a question,
  redirect back to training program questions page instead of contest page.

- Fix announcement editing: TrainingProgramAnnouncementsHandler.post now
  handles both add and edit based on announcement_id parameter.

- Fix announcement removing: Added TrainingProgramAnnouncementHandler with
  delete method and corresponding route. Updated template to use training
  program URL for remove when in training program context.

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

* Fix task archive score to only include official submissions

The score shown in the training program overview should only include
official submissions, consistent with how the ranking calculates scores.

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

* Fix announcement removal redirect and message to student 404

- Fix announcement removal redirect: Changed '../announcements' to 'announcements'
  since the redirect is resolved relative to the current page URL (announcements
  list), not the delete URL.

- Fix message to student 404: Added exception for paths ending with '/message'
  in the prepare() method to prevent rewriting /user/{id}/message URLs to
  non-existent training program URLs.

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

* Fix announcement handler permissions to match contest handlers

Changed TrainingProgramAnnouncementsHandler.post and TrainingProgramAnnouncementHandler.delete
from PERMISSION_ALL to PERMISSION_MESSAGING to match the contest announcement handlers.
This ensures admins with messaging permission can edit/remove announcements in training programs
just like they can in regular contests.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: [email protected] <[email protected]>
…age (#54)

* Add name and description editing fields to training program general page

Co-Authored-By: [email protected] <[email protected]>

* Sync training program description to managing contest

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: [email protected] <[email protected]>
* Hebrew translate pick (#51)

* Translated using Weblate (Hebrew)

Currently translated at 9.3% (29 of 311 strings)

Translation: CMS/main
Translate-URL: https://hosted.weblate.org/projects/cms/main/he/

* Translated using Weblate (Hebrew)

Currently translated at 9.6% (30 of 311 strings)

Translation: CMS/main
Translate-URL: https://hosted.weblate.org/projects/cms/main/he/

* Translated using Weblate (Hebrew)

Currently translated at 55.3% (172 of 311 strings)

Translation: CMS/main
Translate-URL: https://hosted.weblate.org/projects/cms/main/he/

* Change default C++ standard to gnu++20 for .cpp files (#61)

* Change default C++ standard to gnu++20 for .cpp files

Rename the C++20 language from 'C++20 / g++' to 'C++ / g++' so that it
sorts first alphabetically among C++ languages. This makes gnu++20 the
default standard when compiling .cpp files (like manager.cpp and
checker.cpp) since filename_to_language() picks the first matching
language alphabetically.

Updated all references to the language name in:
- cms/grading/languages/cpp20_gpp.py
- cms/db/contest.py (default languages)
- setup.py (entry points)
- cmstestsuite/Tests.py
- cmstestsuite/unit_tests/cmscontrib/AddSubmissionTest.py

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

* Change default C++ standard to gnu++20 for .cpp files

Instead of renaming the C++20 language, use a simpler approach: change
filename_to_language() to pick the last matching language alphabetically
(names[-1]) instead of the first (names[0]).

For C++ languages sorted alphabetically:
- C++11 / g++
- C++14 / g++
- C++17 / g++
- C++20 / g++

This makes 'C++20 / g++' the default when compiling .cpp files like
manager.cpp and checker.cpp, while keeping the language name visible
to users in the UI.

Note: This also affects other languages with multiple implementations
sharing the same extension (e.g., Python 3 / CPython vs Python 3 / PyPy
for .py files - PyPy will now be selected instead of CPython when
language is auto-detected).

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Hide hidden users from attendance tab and CSV export (#62)

Filter out participations with hidden=True from:
- DelaysAndExtraTimesHandler (attendance tab view)
- ExportDelaysAndExtraTimesHandler (CSV export)

This ensures hidden users are not displayed in the attendance tab
and are not included in the exported CSV file.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add subtasks column with optional naming to testcase table (#38)

* Add groups column to testcase table for group-based score types

When a dataset uses a group-based score type (GroupMin, GroupMul, or
GroupThreshold), the testcase table in the admin interface now displays
a 'Groups' column showing which groups (0-based index) each testcase
belongs to. This helps administrators understand the structure of
subtasks when configuring tasks with group-based scoring.

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

* Add subtask naming feature to testcase table

Extract optional subtask names from score type parameters (third element)
and display them in the Subtasks column instead of indices when available.

Example: [[0, "sample", "sample"], [10, "ST1_", "N<=10"], ...]
- If subtask has a name (third element), display the name
- If subtask has no name, fall back to displaying the index

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add bulk user upload feature to contest management (#36)

* Add bulk user upload feature to contest management

- Add BulkAddContestUsersHandler to handle file uploads
- Parse whitespace-separated usernames from .txt files
- Display results table showing status for each username:
  - Success: user added to contest
  - Already exists: user already in contest
  - Not found: username doesn't exist in system
- Add file upload form above 'Add a new user' section
- Register new handler route at /contest/{id}/users/bulk_add

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

* Fix: Initialize bulk_add_results in GET handler to prevent undefined variable

- Set bulk_add_results to None in ContestUsersHandler.get()
- Prevents template errors when accessing the page normally
- Ensures variable is always defined for Jinja template

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add translation feature with Hebrew language code fix and auto-detect (#39)

* Add translation feature to contestants documentation tab

- Add deep-translator dependency to pyproject.toml
- Create TranslationHandler for text translation between languages
- Add translation.html template with form for translation
- Add Translation link to sidebar navigation
- Support English, Hebrew, Russian, and Arabic translations

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

* Fix translation feature errors and add comprehensive tests

- Fix undefined variable error in translation.html template
- Add default values for all template variables in GET method
- Make template more resilient with 'is defined' guards
- Extract translation logic into testable translate_text function
- Add dir='auto' for RTL language support (Hebrew, Arabic)
- Add comprehensive unit tests with 9 test cases
- Mock GoogleTranslator to avoid network dependencies in tests
- Test all validation cases and error handling

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

* Fix Hebrew language code and add auto-detect feature

- Add GOOGLE_TRANSLATE_CODE_MAP to normalize language codes
- Fix Hebrew: map 'he' to 'iw' (Google Translate's code)
- Accept 'iw' as an alias for Hebrew
- Add 'Detect Language' (auto) option for source language
- Make 'auto' the default source language selection
- Reject 'auto' as target language with clear error message
- Add 6 new unit tests for normalization and auto-detection
- Update existing test to verify 'he' normalizes to 'iw'
- All 15 tests passing

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Fix floating point comparison to use correct output only for tolerance (#64)

The _compare_real_pair function was computing tolerance based on both
the user output (a) and the correct output (b). This allowed a malicious
or buggy user output (e.g., inf from a very large number) to inflate
the tolerance, causing incorrect answers to be accepted.

The fix changes the tolerance calculation from:
  tol = eps * max(1.0, abs(a), abs(b))
to:
  tol = eps * max(1.0, abs(b))

This ensures tolerance is only based on the correct output, preventing
user outputs from affecting the comparison threshold.

Added a regression test that verifies large user outputs (parsed as inf)
are correctly rejected.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add task and contest export functionality to admin interface (#47)

* Add contest and task export functionality to admin interface

- Implement ExportTaskHandler to export tasks in YamlLoader format
- Implement ExportContestHandler to export contests with all tasks
- Export structure includes:
  - statements/ folder with PDFs per language
  - attachments/ folder with task attachments
  - tests.zip with input/output testcases
  - managers/ folder with checker, grader, and other managers
  - task.yaml with all task configuration
  - contest.yaml with all contest configuration
- Add export buttons to task and contest admin pages
- Exported zip files are compatible with YamlLoader import

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

* Refactor testcase export to use template-based approach

- Define templates at the beginning: input_*.txt and output_*.txt
- Convert to Python format strings: input_%s.txt and output_%s.txt
- Use templates consistently in both zip creation and YAML export
- Stream testcases directly to zip file using get_file_to_fobj()
- Remove temporary directory for testcases (more efficient)
- Follow the same pattern as dataset.py DownloadTestcasesHandler

Benefits:
- More efficient (no temp directory, direct streaming to zip)
- More maintainable (templates defined once, used everywhere)
- Less memory usage (streams directly instead of writing to disk first)
- Consistent with existing codebase patterns

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

* Update cms/server/admin/handlers/export_handlers.py

Co-authored-by: Daniel Weber <[email protected]>

* Update cms/server/admin/handlers/export_handlers.py

Co-authored-by: Daniel Weber <[email protected]>

* Update cms/server/admin/templates/contest.html

Co-authored-by: Daniel Weber <[email protected]>

* Update cms/server/admin/templates/task.html

Co-authored-by: Daniel Weber <[email protected]>

* Add Communication task params export and filter compiled managers

- Add get_allowed_manager_basenames function to util.py for detecting
  manager basenames that should be auto-compiled
- Export num_processes and user_io for Communication tasks
- Remove redundant output_only field (task_type already captures this)
- Filter managers to export only source files when both source and
  compiled versions exist for the same basename

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

* Add OutputOnly realprecision exponent export

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

* Add BatchAndOutput task type export support

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

* Add allowed_languages export for tasks

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>
Co-authored-by: Daniel Weber <[email protected]>

* Add task handling options when deleting a contest (#30)

* Add task handling options when deleting a contest

- Modified contest_remove.html to show 3 options for task handling:
  1. Move tasks to another contest
  2. Remove tasks from contest but keep in system
  3. Delete all tasks (current behavior)
- Updated RemoveContestHandler to support POST method with task handling logic
- Added _remove_contest_with_action helper method to handle different actions
- Preserved backward compatibility with DELETE method
- Added warning that participations and submissions will be deleted regardless of option chosen

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

* Fix button styling and add unit tests for contest deletion

- Fix button styling inconsistency by using anchor tag with JS form submission for 'Yes, remove contest' button to match 'No, cancel' button style
- Add comprehensive unit tests for contest deletion task handling (move, detach, delete_all)
- Tests use unique identifiers (UUID) to avoid database collisions between test runs

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

* Refactor contest deletion to use DELETE with AJAX and extract testable helper

- Changed contest_remove.html to use CMS.AWSUtils.ajax_delete() pattern instead of POST form submission, aligning with existing CMS removal handlers (task, team, user)
- Modified RemoveContestHandler to use DELETE method with query parameters instead of POST with form data
- Extracted core logic into standalone remove_contest_with_action() helper function for testability
- Refactored all unit tests to call actual implementation instead of reimplementing logic
- Added docstring to gaps test explaining that gaps can occur during imports/manual edits

This addresses PR comments:
- Comment 1 & 2: Aligns with existing CMS DELETE pattern for security and consistency
- Comments 3, 4, 5: Tests now call actual handler code instead of reimplementing
- Comment 6: Added explanation for why gaps test is needed

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

* Update cms/server/admin/handlers/contest.py

Co-authored-by: Daniel Weber <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>
Co-authored-by: Daniel Weber <[email protected]>

* Add password strength validation for admins, users, and contestants using zxcvbn (#63)

* Add password strength validation for admin accounts

- Add zxcvbn library dependency for password strength checking
- Implement validate_password_strength() function that requires minimum
  score of 3 (safely unguessable) for admin passwords
- Integrate validation into _admin_attrs() to check passwords when
  creating new admins or updating existing admin passwords
- Provide helpful feedback including zxcvbn's warnings and suggestions
  when passwords are too weak
- Add WeakPasswordError exception class for password validation failures

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

* Extend password strength validation to users and contestants

- Move password validation to shared cmscommon/crypto.py module
- Add validate_password_strength() and WeakPasswordError to crypto module
- Update admin handler to use shared validation utility
- Add password validation to user creation/update in admin interface
- Add password validation to contestant registration form
- Update get_password() in base handler to accept user_inputs parameter
  for password strength validation

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

* Fix: Add allow_weak_password bypass for tests and imports

- Revert get_password() in base.py to original 3-argument signature
- Move password validation to explicit checks in UserHandler and AddUserHandler
- Add allow_weak_password parameter to bypass validation for programmatic
  user creation (tests, imports, CLI tools)
- Update functional test framework to pass allow_weak_password=1
- Contestant registration still enforces strong passwords (no bypass)

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

* Add user-friendly registration error messages and password strength indicator

- Add RegistrationError exception for specific validation errors
- Return JSON error responses with error codes for registration failures
- Update frontend to display specific error messages for each failure type:
  - Weak password
  - Invalid username characters
  - Invalid username/password length
  - Invalid first/last name
  - Invalid team selection
  - Generic fallback for unknown errors
- Add zxcvbn.js for client-side password strength checking
- Add real-time password strength indicator bar with color coding
- Show strength feedback as user types (Very weak to Very strong)

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

* Add password strength bar to admin interface and participation password validation

- Add password strength validation for participation password changes
- Add zxcvbn.js to admin static directory
- Add password strength indicator bar to admin form (admin creation/editing)
- Add password strength indicator bar to hashed password form (user/participation editing)
- Include zxcvbn.js in admin base template

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

* Address PR review comments: add first/last name to user_inputs, change password field type

- Add first_name and last_name to user_inputs for password validation in ParticipationHandler
- Change password input type from 'text' to 'password' in hashed_password_form.html

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>

* Improve phase management and interface (#49)

* Simplify phase logic

Now that we always have a start button - users that did not start
should not be in analysis mode.
(They should have requested a delay if the contest is not finished
yet)

* Improve contest state description

Fix wrong tenses, countdowns to blocked analysis when user didn't
participate, and add a little data.

* Update phase management tests for simplified phase logic

- Update traditional contest tests with starting_time=None to expect phase -1
  instead of 0 (users who haven't started see phase -1 during contest window)
- Update traditional contest tests with starting_time set to include phase -1
  before actual_start, then phase 0 until contest end
- Update USACO-like tests with starting_time=None to skip analysis phases
  (users who never started jump straight to +4, no analysis mode)
- Update test_usaco_per_user_analysis test with starting_time=None similarly

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

* Add contest_stop <= analysis_stop constraint and improve test coverage

- Add assertion in compute_actual_phase to enforce contest_stop <= analysis_stop
- Fix test that violated the new constraint (changed analysis_stop from 14 to 18)
- Add new test scenarios for traditional contests with starting_time set
  AND overlapping contest/analysis times
- Add boundary test where analysis_stop equals contest_stop

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Add CSV export and import functionality for users (#44)

* Add CSV export functionality for users list

- Add ExportUsersHandler to export all users to CSV
- Include all user fields: first name, last name, username, password, email, timezone, preferred languages
- Password column shows the actual password value (plaintext or hash)
- Plain text / Hash column indicates whether password is stored as plaintext or hashed
- Add 'Export to CSV' button to users list page
- Export is available at /users/export route

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

* Add CSV import functionality for users

- Add ImportUsersHandler to upload and process CSV files
- Add ImportUsersConfirmHandler to finalize user creation/updates
- Validate CSV format and required fields (username, first name, last name, password)
- Show detailed results with three categories:
  * New users to be created
  * Existing users with option to select which to update
  * Failed users with validation error messages
- Support both plaintext and hashed passwords
- Add 'Import from CSV' button to users list page
- Include 'Select All' and 'Deselect All' buttons for existing users
- Show field changes for existing users (old vs new values)
- Only accept CSV files with proper UTF-8 encoding

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

* Fix bug: populate existing_first_name and existing_last_name fields

The import results template was referencing existing_first_name and
existing_last_name to show field changes, but these fields were not
being populated in the ImportUsersHandler. This fix ensures the
comparison display works correctly for all fields.

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

* Fix password handling and add defensive template checks

- Fix critical bug: don't rehash already-hashed passwords
  When CSV contains 'Hash', store password directly as 'bcrypt:hash'
  instead of rehashing it with build_password()
- Add 'is defined' checks in template to prevent UndefinedError
  if fields are missing (defensive programming)
- This ensures template won't crash even if running older handler code

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

* Rename import results page to confirm page

- Rename import_users_results.html to import_users_confirm.html
- Update page title from 'Import Users - Results' to 'Import Users - Confirm'
- Update handler to reference new template name
- Better reflects that this is a confirmation page shown before import,
  not a results page shown after import is complete

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

* Address PR feedback: improve password type handling and CSV format

- Export: Set password_type to 'Unknown' instead of 'Hash' when parse_authentication fails
- Export: Use semicolon ('; ') instead of comma to separate preferred languages
- Import: Add validation to reject 'Unknown' password types with clear error message
- Import: Accept both semicolon and comma separators for preferred languages (backward compatible)

This addresses feedback from PR review comments:
- Prevents misclassifying unknown password formats as bcrypt hashes
- Makes CSV cleaner and easier to work with
- Maintains backward compatibility for existing CSVs with comma separators

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

* Improve security: hash plaintext passwords on import

- Change import to use hash_password() instead of build_password() for plaintext passwords
- Plaintext passwords are now hashed with bcrypt on import for better security
- Add security note to import form explaining this behavior
- Update preferred languages note to mention both semicolon and comma separators

This addresses feedback from ronryv that improving security is more important
than preserving plaintext password storage format.

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

* Add password strength bar to add_user.html

Adds the same password strength indicator that was added in PR #63 to the
add_user.html template. Changes include:
- Changed password input type from 'text' to 'password' for security
- Added password strength bar with real-time feedback using zxcvbn
- Uses unique element IDs (add-user-password-*) to avoid conflicts

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add delay requests to participation page and make usernames clickable in attendance page (#65)

* Add delay requests to participation page and make usernames clickable in attendance page

- Add a new 'Delay Requests' section to the participation page that displays
  all delay requests for that user in the contest (similar to how questions
  are displayed)
- Make usernames clickable links to the participation page in the attendance
  page table (both User and Username columns)
- Make usernames clickable in the pending delay requests list
- Make usernames clickable in the all delay requests list
- Use subtle link styling that inherits the text color and only shows
  underline on hover to maintain the table's appearance

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

* Create shared delay request macro and fix link styling

- Create a new delay_request.html macro (similar to question.html) that can be
  shared between the participation page and attendance page
- The macro shows delay requests with timestamp, requested start time, reason,
  status, and approve/reject buttons for pending requests
- When user_id is None (attendance page), show a link to the user's participation
  page; when user_id is set (participation page), don't show the link
- Fix CSS for username links to properly override body.admin a color by using
  more specific selectors (a.username-link, a.username-link:link, etc.)
- Update both pages to use the shared macro for consistent display

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

* Improve GUI

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add task statement view tracking and ranking indicators (#37)

* Add task statement view tracking and ranking indicators

- Add StatementView database model to track when contestants first view task statements
- Update TaskStatementViewHandler to record statement views in the database
- Extend ranking table to show three indicators:
  * '*' for tasks with submissions being evaluated (existing)
  * 'X' for tasks where statement hasn't been opened yet (new)
  * '-' for tasks with no submissions (new)
- Update RankingHandler to compute indicators with proper precedence
- Update ranking.html, ranking.txt, and CSV export to display indicators
- Add proper database relationships between StatementView, Participation, and Task

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

* Fix CI failures: add statement_views to import spec

- Add Task.statement_views: False to update specification in importing.py
  to mark it as a non-imported relationship (runtime telemetry)
- Add IntegrityError handling in TaskStatementViewHandler to handle
  race conditions when multiple requests try to insert the first view

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

* Add statement_views table DDL to SQL updater

Add CREATE TABLE and related DDL statements for the statement_views
table to the update_from_1.5.sql file. This ensures that the upgraded
database schema matches the fresh schema in the schema_diff_test.

The DDL follows the same pattern as the delay_requests table, including:
- Table creation with columns
- Sequence creation for auto-increment ID
- Primary key constraint
- Unique constraint on (participation_id, task_id)
- Indexes on foreign key columns
- Foreign key constraints with CASCADE options

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

* Update ranking indicators with emoji display and new logic

- HTML ranking now uses emojis:
  - 🙈 for no download + no submissions (omit score)
  - 💤 for downloaded + no submissions (omit score)
  - ⚠️score*🙈⚠️ for submitted without download (inline star if partial)
  - score with CSS star for normal case (if partial)
- Export indicators (CSV/TXT) now use:
  - X for no download + no submission
  - - for downloaded + no submission
  - ! for submitted without download
  - !* for submitted without download + partial
  - * for partial only
  - empty for normal
- Fixed CSS class usage to avoid unwanted trailing stars
- Separated HTML display (scores_html) from export data (scores_export)

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

* Update export filenames to use date and contest name format

- CSV/TXT export filenames now use format: yyyymmdd_contestname.csv/txt
- Date is formatted from contest start date (YYYYMMDD)
- Contest name has spaces replaced with underscores
- Example: 20250114_MyContest.csv

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

* Move formatting to html

* Add 'show indicators' checkbox to ranking page

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add Training Programs feature (Phase 1) with admin interface improvements (#52)

* Add Training Programs feature (Phase 1)

This commit introduces the Training Programs feature for organizing
year-long training with multiple weekly contests.

Phase 1 includes:
- TrainingProgram database model with managing contest relationship
- Database migrations (version 47 -> 48)
- Admin CRUD handlers and templates for training programs
- Training Programs navigation entry in admin sidebar
- Basic CWS training program overview page with score tracking

The training program creates a managing contest (prefixed with __) that
handles all submissions and user participations for the program.

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

* Fix CI failures: add training_program to import spec and fix schema

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

* Improve training program overview page with filtering, sorting, and progress bar

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

* Hide system users/contests and prevent manual creation with __ prefix (#41)

* Hide system users and contests from admin lists and prevent manual creation

- Filter out users/contests starting with '__' from admin list views
- Add validation to prevent creating users with '__' prefix in:
  - Registration handler (contest web server)
  - Admin user creation handler
  - Admin user edit handler
- Add validation to prevent creating contests with '__' prefix in:
  - Admin contest creation handler
  - Admin contest edit handler
- Fix line length issues to comply with flake8

This ensures that system entities like __model_solutions__ user and
__model_solutions_system__ contest remain hidden from admins and cannot
be accidentally created or modified through the UI.

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

* Fix: Hide system users/contests from unassigned dropdowns

- Filter unassigned_users in ContestUsersHandler to exclude users starting with '__'
- Filter unassigned_contests in UserHandler to exclude contests starting with '__'
- Use proper SQL LIKE with escape to avoid wildcard issues

This fixes the issue where __model_solutions__ user was visible in the
'Add a new user' dropdown when adding users to a contest, and ensures
__model_solutions_system__ contest doesn't appear when adding participations.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Fix XSRF error on training program delete by using CMS.AWSUtils.ajax_delete

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

* Merge remote changes and add training program URL routing, browse page, and default overview redirect

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

* Add Student model and training program admin interface improvements

- Create Student model with student_tags (list of strings) for training program participation
- Add SQL migration for students table in update_from_1.5.sql
- Update sidebar in base.html to show training programs list like contests
- Add training program detail page with tabs: General, Ranking, Submissions, Students, Tasks, Announcements, Questions
- Create handlers for student management that sync with managing contest's Participation
- Create templates for students and tasks pages
- Register all new routes in handlers/__init__.py

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

* Fix SQL migration schema mismatch for students.participation_id

Use a UNIQUE INDEX instead of separate UNIQUE constraint and regular index
to match SQLAlchemy's schema generation.

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

* Fix 4 bugs in training program admin interface

1. Fix student tags error by restructuring template to avoid nested forms
2. Fix announcement redirect by checking for training_program context in template
3. Fix ranking page crash by duplicating ranking logic from RankingHandler
4. Fix side menu visibility by updating base.html conditional

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

* Add student detail page and redirect managing contest URLs to training programs

- Create ManagingContestRedirectHandler to redirect /contest/{managing_contest_id}/* URLs to /training_program/{id}/*
- Create StudentHandler to show and edit student details including tags
- Create student.html template based on participation.html with student tags field
- Update training_program_students.html to link to student page instead of participation page
- Remove UpdateStudentTagsHandler as it's now redundant (tags edited on student page)
- Remove 'Update Student Tags' section from students list

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

* Fix CI: Move managing contest redirect logic from catch-all route to BaseHandler.prepare()

The ManagingContestRedirectHandler was intercepting all /contest/{id}/* URLs
and breaking normal contest functionality by raising 404 for non-managing
contests. This caused the folder_functional_test to fail.

The fix moves the redirect logic to BaseHandler.prepare() which is called
before any request handler method. This allows normal contest handlers to
work while still redirecting managing contest URLs to training program URLs.

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

* Fix 403 error when accessing training program tasks

Skip the starting_time check for training programs since they are
meant to be always accessible (year-long training, not time-limited
contests). Normal contests still require users to 'start' before
accessing tasks.

Affected handlers:
- TaskDescriptionHandler
- TaskStatementViewHandler
- TaskAttachmentViewHandler
- SubmitHandler
- TaskSubmissionsHandler
- SubmissionStatusHandler
- SubmissionDetailsHandler
- SubmissionFileHandler
- UseTokenHandler

Co-Authored-By: [email protected] <[email protected]>

* Fix training program removal: add POST handler to redirect to confirmation page

Co-Authored-By: [email protected] <[email protected]>

* Add navigation links between task description and submissions pages

- Add Submit column with link to submissions page in training program task archive
- Add 'Go to submissions' button at bottom of task description pages
- Add 'Go to statement' button at top of task submissions pages

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

* Fix training program removal: add missing 'contest' and 'unanswered' variables for base template

Co-Authored-By: [email protected] <[email protected]>

* Fix Overview/Resource usage 404 and Questions link on training program pages

- Use global Overview/Resource usage links when training_program is defined
- Add unanswered variable to TrainingProgramHandler for Questions link count

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

* Remove managing contest links and link users to student pages on General page

- Remove Managing Contest row from the form table
- Remove Managing Contest Details section with instructions
- Change Users section to Students section
- Link student names to student detail page instead of user page

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

* Fix sidebar for tasks/submissions in training programs

When viewing a task or submission that belongs to a training program's
managing contest, automatically set training_program in render_params
so the sidebar shows the training program menu instead of the managing
contest menu.

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

* Fix error 405 when removing student from training program

The template was using a POST form but the handler only has a DELETE
method. Changed to use ajax_delete like other remove templates.

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

* Fix NotNullViolation when removing student from training program

Delete the Student record before deleting the Participation to avoid
the NOT NULL constraint violation on students.participation_id.

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

* Fix question replies and announcement editing/removing in training programs

- Fix 404 when replying to questions: The prepare() method was redirecting
  /contest/{id}/question/... URLs to training program URLs which don't exist.
  Added exception for question and announcement paths to allow them to use
  the contest handlers directly.

- Fix question action redirects: After replying/ignoring/claiming a question,
  redirect back to training program questions page instead of contest page.

- Fix announcement editing: TrainingProgramAnnouncementsHandler.post now
  handles both add and edit based on announcement_id parameter.

- Fix announcement removing: Added TrainingProgramAnnouncementHandler with
  delete method and corresponding route. Updated template to use training
  program URL for remove when in training program context.

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

* Fix task archive score to only include official submissions

The score shown in the training program overview should only include
official submissions, consistent with how the ranking calculates scores.

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

* Fix announcement removal redirect and message to student 404

- Fix announcement removal redirect: Changed '../announcements' to 'announcements'
  since the redirect is resolved relative to the current page URL (announcements
  list), not the delete URL.

- Fix message to student 404: Added exception for paths ending with '/message'
  in the prepare() method to prevent rewriting /user/{id}/message URLs to
  non-existent training program URLs.

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

* Fix announcement handler permissions to match contest handlers

Changed TrainingProgramAnnouncementsHandler.post and TrainingProgramAnnouncementHandler.delete
from PERMISSION_ALL to PERMISSION_MESSAGING to match the contest announcement handlers.
This ensures admins with messaging permission can edit/remove announcements in training programs
just like they can in regular contests.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: [email protected] <[email protected]>

* Add name and description editing fields to training program general page (#54)

* Add name and description editing fields to training program general page

Co-Authored-By: [email protected] <[email protected]>

* Sync training program description to managing contest

Co-Authored-By: [email protected] <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: [email protected] <[email protected]>

* fix contest html (#58)

* Fix training program ranking page to use task_statuses

The ranking.html template expects participations to have a task_statuses
attribute with TaskStatus namedtuples containing score, partial,
has_submissions, and has_opened fields. The TrainingProgramRankingHandler
was incorrectly using p.scores instead of p.task_statuses.

Changes:
- Import TaskStatus from contestranking module
- Add statement_views to joinedload for tracking opened statements
- Build statement_views_set to track which statements were opened
- Change p.scores to p.task_statuses with proper TaskStatus objects
- Update CSV export to use p.task_statuses instead of p.scores
- Add _status_indicator method for CSV export formatting

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

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Daniel Weber <[email protected]>
Co-authored-by: [email protected] <[email protected]>
…55)

* Add TrainingDay model and admin interface for managing training days

- Create TrainingDay model that wraps a Contest and belongs to a TrainingProgram
- Add position field for ordering training days within a program
- Add training_days relationship to TrainingProgram with ordering
- Add training_day back-reference to Contest
- Add TrainingProgramTrainingDaysHandler for listing training days
- Add AddTrainingDayHandler for creating new training days
- Add RemoveTrainingDayHandler for deleting training days
- Add Training Days link to sidebar navigation
- Create templates for training days list, add, and remove pages
- Increment database version to 49

Co-Authored-By: [email protected] <[email protected]>

* Fix CI: Add update_49.py, update importing.py spec, and add training_days schema

Co-Authored-By: [email protected] <[email protected]>

* Add unique constraints, fix xsrf_token error, and implement up/down reordering for training days

- Add UniqueConstraint on (training_program_id, position) to prevent duplicate positions
- Fix xsrf_token undefined error in training_day_remove.html by using CMS.AWSUtils.ajax_delete
- Remove position column from training days list UI
- Implement up/down reordering mechanic for training days

Co-Authored-By: [email protected] <[email protected]>

* Auto-add participations for training day students and rename Users to Students

- When creating a training day, automatically create Participation records
  for all students in the training program (no manual user addition needed)
- Rename 'Users' column to 'Students' in training programs list for consistency

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

* Sync student participations with training days and hide Users tab

- When adding a student to training program, also add them to all existing training days
- When removing a student, also remove from all training days with warning about submissions
- Hide 'Users' tab in sidebar when viewing a training day's contest (users are managed via training program)

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

* Add task management for training days

- Add training_day_id foreign key to Task model to support training day-specific tasks
- Add tasks relationship to TrainingDay model
- Add get_tasks() method to Contest that returns training day tasks if applicable
- Update admin interface (ContestTasksHandler, AddContestTaskHandler) to:
  - Show only training program tasks when adding tasks to a training day
  - Add tasks to training day instead of contest when in training day context
  - Show appropriate UI labels for training day vs regular contest
- Update contest web server templates and handlers to use get_tasks()
- Add database migration for new training_day_id column in tasks table

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

* Fix CI: Add training_day relationship to Task update specification in importing.py

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

* Fix task management design: tasks keep contest_id and use training_day_num for ordering

- Tasks keep their contest_id (managing contest) when assigned to a training day
- Added training_day_num column for ordering tasks within training days (separate from contest num)
- Updated admin handler to use SQL query for unassigned tasks
- Fixed get_task() method to work with training day tasks
- Updated TrainingDay.tasks relationship to order by training_day_num

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

* Fix assertion errors for training day tasks

- Added task_belongs_here() method to Contest model to check if a task
  belongs to a contest (either directly via contest_id or via training_day_id)
- Updated assertions in tokening.py and workflow.py to use the new method
- This fixes the AssertionError when contestants access training day tasks

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

* Add training program with breadcrumbs to modern side menu (#78)

* Hebrew translate pick (#51)

* Translated using Weblate (Hebrew)

Currently translated at 9.3% (29 of 311 strings)

Translation: CMS/main
Translate-URL: https://hosted.weblate.org/projects/cms/main/he/

* Translated using Weblate (Hebrew)

Currently translated at 9.6% (30 of 311 strings)

Translation: CMS/main
Translate-URL: https://hosted.weblate.org/projects/cms/main/he/

* Translated using Weblate (Hebrew)

Currently translated at 55.3% (172 of 311 strings)

Translation: CMS/main
Translate-URL: https://hosted.weblate.org/projects/cms/main/he/

* Change default C++ standard to gnu++20 for .cpp files (#61)

* Change default C++ standard to gnu++20 for .cpp files

Rename the C++20 language from 'C++20 / g++' to 'C++ / g++' so that it
sorts first alphabetically among C++ languages. This makes gnu++20 the
default standard when compiling .cpp files (like manager.cpp and
checker.cpp) since filename_to_language() picks the first matching
language alphabetically.

Updated all references to the language name in:
- cms/grading/languages/cpp20_gpp.py
- cms/db/contest.py (default languages)
- setup.py (entry points)
- cmstestsuite/Tests.py
- cmstestsuite/unit_tests/cmscontrib/AddSubmissionTest.py

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

* Change default C++ standard to gnu++20 for .cpp files

Instead of renaming the C++20 language, use a simpler approach: change
filename_to_language() to pick the last matching language alphabetically
(names[-1]) instead of the first (names[0]).

For C++ languages sorted alphabetically:
- C++11 / g++
- C++14 / g++
- C++17 / g++
- C++20 / g++

This makes 'C++20 / g++' the default when compiling .cpp files like
manager.cpp and checker.cpp, while keeping the language name visible
to users in the UI.

Note: This also affects other languages with multiple implementations
sharing the same extension (e.g., Python 3 / CPython vs Python 3 / PyPy
for .py files - PyPy will now be selected instead of CPython when
language is auto-detected).

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Hide hidden users from attendance tab and CSV export (#62)

Filter out participations with hidden=True from:
- DelaysAndExtraTimesHandler (attendance tab view)
- ExportDelaysAndExtraTimesHandler (CSV export)

This ensures hidden users are not displayed in the attendance tab
and are not included in the exported CSV file.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add subtasks column with optional naming to testcase table (#38)

* Add groups column to testcase table for group-based score types

When a dataset uses a group-based score type (GroupMin, GroupMul, or
GroupThreshold), the testcase table in the admin interface now displays
a 'Groups' column showing which groups (0-based index) each testcase
belongs to. This helps administrators understand the structure of
subtasks when configuring tasks with group-based scoring.

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

* Add subtask naming feature to testcase table

Extract optional subtask names from score type parameters (third element)
and display them in the Subtasks column instead of indices when available.

Example: [[0, "sample", "sample"], [10, "ST1_", "N<=10"], ...]
- If subtask has a name (third element), display the name
- If subtask has no name, fall back to displaying the index

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add bulk user upload feature to contest management (#36)

* Add bulk user upload feature to contest management

- Add BulkAddContestUsersHandler to handle file uploads
- Parse whitespace-separated usernames from .txt files
- Display results table showing status for each username:
  - Success: user added to contest
  - Already exists: user already in contest
  - Not found: username doesn't exist in system
- Add file upload form above 'Add a new user' section
- Register new handler route at /contest/{id}/users/bulk_add

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

* Fix: Initialize bulk_add_results in GET handler to prevent undefined variable

- Set bulk_add_results to None in ContestUsersHandler.get()
- Prevents template errors when accessing the page normally
- Ensures variable is always defined for Jinja template

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add translation feature with Hebrew language code fix and auto-detect (#39)

* Add translation feature to contestants documentation tab

- Add deep-translator dependency to pyproject.toml
- Create TranslationHandler for text translation between languages
- Add translation.html template with form for translation
- Add Translation link to sidebar navigation
- Support English, Hebrew, Russian, and Arabic translations

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

* Fix translation feature errors and add comprehensive tests

- Fix undefined variable error in translation.html template
- Add default values for all template variables in GET method
- Make template more resilient with 'is defined' guards
- Extract translation logic into testable translate_text function
- Add dir='auto' for RTL language support (Hebrew, Arabic)
- Add comprehensive unit tests with 9 test cases
- Mock GoogleTranslator to avoid network dependencies in tests
- Test all validation cases and error handling

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

* Fix Hebrew language code and add auto-detect feature

- Add GOOGLE_TRANSLATE_CODE_MAP to normalize language codes
- Fix Hebrew: map 'he' to 'iw' (Google Translate's code)
- Accept 'iw' as an alias for Hebrew
- Add 'Detect Language' (auto) option for source language
- Make 'auto' the default source language selection
- Reject 'auto' as target language with clear error message
- Add 6 new unit tests for normalization and auto-detection
- Update existing test to verify 'he' normalizes to 'iw'
- All 15 tests passing

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Fix floating point comparison to use correct output only for tolerance (#64)

The _compare_real_pair function was computing tolerance based on both
the user output (a) and the correct output (b). This allowed a malicious
or buggy user output (e.g., inf from a very large number) to inflate
the tolerance, causing incorrect answers to be accepted.

The fix changes the tolerance calculation from:
  tol = eps * max(1.0, abs(a), abs(b))
to:
  tol = eps * max(1.0, abs(b))

This ensures tolerance is only based on the correct output, preventing
user outputs from affecting the comparison threshold.

Added a regression test that verifies large user outputs (parsed as inf)
are correctly rejected.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add task and contest export functionality to admin interface (#47)

* Add contest and task export functionality to admin interface

- Implement ExportTaskHandler to export tasks in YamlLoader format
- Implement ExportContestHandler to export contests with all tasks
- Export structure includes:
  - statements/ folder with PDFs per language
  - attachments/ folder with task attachments
  - tests.zip with input/output testcases
  - managers/ folder with checker, grader, and other managers
  - task.yaml with all task configuration
  - contest.yaml with all contest configuration
- Add export buttons to task and contest admin pages
- Exported zip files are compatible with YamlLoader import

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

* Refactor testcase export to use template-based approach

- Define templates at the beginning: input_*.txt and output_*.txt
- Convert to Python format strings: input_%s.txt and output_%s.txt
- Use templates consistently in both zip creation and YAML export
- Stream testcases directly to zip file using get_file_to_fobj()
- Remove temporary directory for testcases (more efficient)
- Follow the same pattern as dataset.py DownloadTestcasesHandler

Benefits:
- More efficient (no temp directory, direct streaming to zip)
- More maintainable (templates defined once, used everywhere)
- Less memory usage (streams directly instead of writing to disk first)
- Consistent with existing codebase patterns

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

* Update cms/server/admin/handlers/export_handlers.py

Co-authored-by: Daniel Weber <[email protected]>

* Update cms/server/admin/handlers/export_handlers.py

Co-authored-by: Daniel Weber <[email protected]>

* Update cms/server/admin/templates/contest.html

Co-authored-by: Daniel Weber <[email protected]>

* Update cms/server/admin/templates/task.html

Co-authored-by: Daniel Weber <[email protected]>

* Add Communication task params export and filter compiled managers

- Add get_allowed_manager_basenames function to util.py for detecting
  manager basenames that should be auto-compiled
- Export num_processes and user_io for Communication tasks
- Remove redundant output_only field (task_type already captures this)
- Filter managers to export only source files when both source and
  compiled versions exist for the same basename

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

* Add OutputOnly realprecision exponent export

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

* Add BatchAndOutput task type export support

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

* Add allowed_languages export for tasks

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>
Co-authored-by: Daniel Weber <[email protected]>

* Add task handling options when deleting a contest (#30)

* Add task handling options when deleting a contest

- Modified contest_remove.html to show 3 options for task handling:
  1. Move tasks to another contest
  2. Remove tasks from contest but keep in system
  3. Delete all tasks (current behavior)
- Updated RemoveContestHandler to support POST method with task handling logic
- Added _remove_contest_with_action helper method to handle different actions
- Preserved backward compatibility with DELETE method
- Added warning that participations and submissions will be deleted regardless of option chosen

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

* Fix button styling and add unit tests for contest deletion

- Fix button styling inconsistency by using anchor tag with JS form submission for 'Yes, remove contest' button to match 'No, cancel' button style
- Add comprehensive unit tests for contest deletion task handling (move, detach, delete_all)
- Tests use unique identifiers (UUID) to avoid database collisions between test runs

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

* Refactor contest deletion to use DELETE with AJAX and extract testable helper

- Changed contest_remove.html to use CMS.AWSUtils.ajax_delete() pattern instead of POST form submission, aligning with existing CMS removal handlers (task, team, user)
- Modified RemoveContestHandler to use DELETE method with query parameters instead of POST with form data
- Extracted core logic into standalone remove_contest_with_action() helper function for testability
- Refactored all unit tests to call actual implementation instead of reimplementing logic
- Added docstring to gaps test explaining that gaps can occur during imports/manual edits

This addresses PR comments:
- Comment 1 & 2: Aligns with existing CMS DELETE pattern for security and consistency
- Comments 3, 4, 5: Tests now call actual handler code instead of reimplementing
- Comment 6: Added explanation for why gaps test is needed

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

* Update cms/server/admin/handlers/contest.py

Co-authored-by: Daniel Weber <[email protected]>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>
Co-authored-by: Daniel Weber <[email protected]>

* Add password strength validation for admins, users, and contestants using zxcvbn (#63)

* Add password strength validation for admin accounts

- Add zxcvbn library dependency for password strength checking
- Implement validate_password_strength() function that requires minimum
  score of 3 (safely unguessable) for admin passwords
- Integrate validation into _admin_attrs() to check passwords when
  creating new admins or updating existing admin passwords
- Provide helpful feedback including zxcvbn's warnings and suggestions
  when passwords are too weak
- Add WeakPasswordError exception class for password validation failures

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

* Extend password strength validation to users and contestants

- Move password validation to shared cmscommon/crypto.py module
- Add validate_password_strength() and WeakPasswordError to crypto module
- Update admin handler to use shared validation utility
- Add password validation to user creation/update in admin interface
- Add password validation to contestant registration form
- Update get_password() in base handler to accept user_inputs parameter
  for password strength validation

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

* Fix: Add allow_weak_password bypass for tests and imports

- Revert get_password() in base.py to original 3-argument signature
- Move password validation to explicit checks in UserHandler and AddUserHandler
- Add allow_weak_password parameter to bypass validation for programmatic
  user creation (tests, imports, CLI tools)
- Update functional test framework to pass allow_weak_password=1
- Contestant registration still enforces strong passwords (no bypass)

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

* Add user-friendly registration error messages and password strength indicator

- Add RegistrationError exception for specific validation errors
- Return JSON error responses with error codes for registration failures
- Update frontend to display specific error messages for each failure type:
  - Weak password
  - Invalid username characters
  - Invalid username/password length
  - Invalid first/last name
  - Invalid team selection
  - Generic fallback for unknown errors
- Add zxcvbn.js for client-side password strength checking
- Add real-time password strength indicator bar with color coding
- Show strength feedback as user types (Very weak to Very strong)

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

* Add password strength bar to admin interface and participation password validation

- Add password strength validation for participation password changes
- Add zxcvbn.js to admin static directory
- Add password strength indicator bar to admin form (admin creation/editing)
- Add password strength indicator bar to hashed password form (user/participation editing)
- Include zxcvbn.js in admin base template

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

* Address PR review comments: add first/last name to user_inputs, change password field type

- Add first_name and last_name to user_inputs for password validation in ParticipationHandler
- Change password input type from 'text' to 'password' in hashed_password_form.html

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>

* Improve phase management and interface (#49)

* Simplify phase logic

Now that we always have a start button - users that did not start
should not be in analysis mode.
(They should have requested a delay if the contest is not finished
yet)

* Improve contest state description

Fix wrong tenses, countdowns to blocked analysis when user didn't
participate, and add a little data.

* Update phase management tests for simplified phase logic

- Update traditional contest tests with starting_time=None to expect phase -1
  instead of 0 (users who haven't started see phase -1 during contest window)
- Update traditional contest tests with starting_time set to include phase -1
  before actual_start, then phase 0 until contest end
- Update USACO-like tests with starting_time=None to skip analysis phases
  (users who never started jump straight to +4, no analysis mode)
- Update test_usaco_per_user_analysis test with starting_time=None similarly

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

* Add contest_stop <= analysis_stop constraint and improve test coverage

- Add assertion in compute_actual_phase to enforce contest_stop <= analysis_stop
- Fix test that violated the new constraint (changed analysis_stop from 14 to 18)
- Add new test scenarios for traditional contests with starting_time set
  AND overlapping contest/analysis times
- Add boundary test where analysis_stop equals contest_stop

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Add CSV export and import functionality for users (#44)

* Add CSV export functionality for users list

- Add ExportUsersHandler to export all users to CSV
- Include all user fields: first name, last name, username, password, email, timezone, preferred languages
- Password column shows the actual password value (plaintext or hash)
- Plain text / Hash column indicates whether password is stored as plaintext or hashed
- Add 'Export to CSV' button to users list page
- Export is available at /users/export route

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

* Add CSV import functionality for users

- Add ImportUsersHandler to upload and process CSV files
- Add ImportUsersConfirmHandler to finalize user creation/updates
- Validate CSV format and required fields (username, first name, last name, password)
- Show detailed results with three categories:
  * New users to be created
  * Existing users with option to select which to update
  * Failed users with validation error messages
- Support both plaintext and hashed passwords
- Add 'Import from CSV' button to users list page
- Include 'Select All' and 'Deselect All' buttons for existing users
- Show field changes for existing users (old vs new values)
- Only accept CSV files with proper UTF-8 encoding

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

* Fix bug: populate existing_first_name and existing_last_name fields

The import results template was referencing existing_first_name and
existing_last_name to show field changes, but these fields were not
being populated in the ImportUsersHandler. This fix ensures the
comparison display works correctly for all fields.

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

* Fix password handling and add defensive template checks

- Fix critical bug: don't rehash already-hashed passwords
  When CSV contains 'Hash', store password directly as 'bcrypt:hash'
  instead of rehashing it with build_password()
- Add 'is defined' checks in template to prevent UndefinedError
  if fields are missing (defensive programming)
- This ensures template won't crash even if running older handler code

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

* Rename import results page to confirm page

- Rename import_users_results.html to import_users_confirm.html
- Update page title from 'Import Users - Results' to 'Import Users - Confirm'
- Update handler to reference new template name
- Better reflects that this is a confirmation page shown before import,
  not a results page shown after import is complete

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

* Address PR feedback: improve password type handling and CSV format

- Export: Set password_type to 'Unknown' instead of 'Hash' when parse_authentication fails
- Export: Use semicolon ('; ') instead of comma to separate preferred languages
- Import: Add validation to reject 'Unknown' password types with clear error message
- Import: Accept both semicolon and comma separators for preferred languages (backward compatible)

This addresses feedback from PR review comments:
- Prevents misclassifying unknown password formats as bcrypt hashes
- Makes CSV cleaner and easier to work with
- Maintains backward compatibility for existing CSVs with comma separators

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

* Improve security: hash plaintext passwords on import

- Change import to use hash_password() instead of build_password() for plaintext passwords
- Plaintext passwords are now hashed with bcrypt on import for better security
- Add security note to import form explaining this behavior
- Update preferred languages note to mention both semicolon and comma separators

This addresses feedback from ronryv that improving security is more important
than preserving plaintext password storage format.

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

* Add password strength bar to add_user.html

Adds the same password strength indicator that was added in PR #63 to the
add_user.html template. Changes include:
- Changed password input type from 'text' to 'password' for security
- Added password strength bar with real-time feedback using zxcvbn
- Uses unique element IDs (add-user-password-*) to avoid conflicts

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add delay requests to participation page and make usernames clickable in attendance page (#65)

* Add delay requests to participation page and make usernames clickable in attendance page

- Add a new 'Delay Requests' section to the participation page that displays
  all delay requests for that user in the contest (similar to how questions
  are displayed)
- Make usernames clickable links to the participation page in the attendance
  page table (both User and Username columns)
- Make usernames clickable in the pending delay requests list
- Make usernames clickable in the all delay requests list
- Use subtle link styling that inherits the text color and only shows
  underline on hover to maintain the table's appearance

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

* Create shared delay request macro and fix link styling

- Create a new delay_request.html macro (similar to question.html) that can be
  shared between the participation page and attendance page
- The macro shows delay requests with timestamp, requested start time, reason,
  status, and approve/reject buttons for pending requests
- When user_id is None (attendance page), show a link to the user's participation
  page; when user_id is set (participation page), don't show the link
- Fix CSS for username links to properly override body.admin a color by using
  more specific selectors (a.username-link, a.username-link:link, etc.)
- Update both pages to use the shared macro for consistent display

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

* Improve GUI

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Add task statement view tracking and ranking indicators (#37)

* Add task statement view tracking and ranking indicators

- Add StatementView database model to track when contestants first view task statements
- Update TaskStatementViewHandler to record statement views in the database
- Extend ranking table to show three indicators:
  * '*' for tasks with submissions being evaluated (existing)
  * 'X' for tasks where statement hasn't been opened yet (new)
  * '-' for tasks with no submissions (new)
- Update RankingHandler to compute indicators with proper precedence
- Update ranking.html, ranking.txt, and CSV export to display indicators
- Add proper database relationships between StatementView, Participation, and Task

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

* Fix CI failures: add statement_views to import spec

- Add Task.statement_views: False to update specification in importing.py
  to mark it as a non-imported relationship (runtime telemetry)
- Add IntegrityError handling in TaskStatementViewHandler to handle
  race conditions when multiple requests try to insert the first view

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

* Add statement_views table DDL to SQL updater

Add CREATE TABLE and related DDL statements for the statement_views
table to the update_from_1.5.sql file. This ensures that the upgraded
database schema matches the fresh schema in the schema_diff_test.

The DDL follows the same pattern as the delay_requests table, including:
- Table creation with columns
- Sequence creation for auto-increment ID
- Primary key constraint
- Unique constraint on (participation_id, task_id)
- Indexes on foreign key columns
- Foreign key constraints with CASCADE options

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

* Update ranking indicators with emoji display and new logic

- HTML ranking now uses emojis:
  - 🙈 for no download + no submissions (omit score)
  - 💤 for downloaded + no submissions (omit score)
  - ⚠️score*🙈⚠️ for submitted without download (inline star if partial)
  - score with CSS star for normal case (if partial)
- Export indicators (CSV/TXT) now use:
  - X for no download + no submission
  - - for downloaded + no submission
  - ! for submitted without download
  - !* for submitted without download + partial
  - * for partial only
  - empty for normal
- Fixed CSS class usage to avoid unwanted trailing stars
- Separated HTML display (scores_html) from export data (scores_export)

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

* Update export filenames to use date and contest name format

- CSV/TXT export filenames now use format: yyyymmdd_contestname.csv/txt
- Date is formatted from contest start date (YYYYMMDD)
- Contest name has spaces replaced with underscores
- Example: 20250114_MyContest.csv

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

* Move formatting to html

* Add 'show indicators' checkbox to ranking page

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>

* Modernize admin sidebar with collapsible folder hierarchy

- Restructure sidebar to show training days (contests) under their training programs (folders)
- Add collapsible folder sections with expand/collapse functionality
- Replace ASCII symbols with modern CSS-based visual hierarchy
- Add breadcrumb navigation showing folder > contest path when viewing a contest
- Update all sidebar sections (Contests, Tasks, Users, Teams) with consistent modern styling
- Add folder data to render_params for hierarchical display
- Improve visual differentiation between main categories and sub-options

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

* Modernize sidebar: fix alignment, remove ASCII symbols, add collapsible training programs

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

* Fix sidebar issues: text alignment, overflow, hidden folders, manage folders link

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

* Modernize sidebar styling: separator, nested folders, typography, scrollbar

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

* Modernize color scheme: replace green with slate blue palette

- Replace green accent (#6DC942) with blue (#2563EB)
- Update sidebar background from green-tinted to clean slate (#F8FAFC)
- Change section titles and headings to deep blue (#1E40AF)
- Update hover effects to subtle blue tint
- Replace green scrollbar with neutral slate gray
- Update all borders and separators to slate colors
- Maintain all existing layout, sizing, and functionality

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

* Implement dark sidebar theme based on user reference

- Dark navy background (#111827) for sidebar
- Light gray text (#E5E7EB) for menu items and folders
- Muted gray (#64748B) for section headers with uppercase styling
- Subtle hover effects with slate tint
- Light blue accents (#60A5FA) for breadcrumbs
- Dark borders (#1F2937, #334155) for separators
- Updated scrollbar to match dark theme

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

* Update color scheme: teal for main content, bright white for sidebar

Main content area:
- Headings changed to dark gray (#1F2937) for better readability
- Links changed to teal (#0F766E) per user reference
- Button borders and info badges updated to teal theme
- Hover backgrounds use light teal (#F0FDFA)

Sidebar (dark theme):
- Added specific rules to keep sidebar links bright white (#E5E7EB)
- Hover state brightens to near-white (#F9FAFB)
- Prevents teal from bleeding into dark sidebar

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

* Fix sidebar visibility and improve hover consistency

Visibility fixes:
- Add base text color (#CBD5E1) to sidebar for plain text visibility
- Add explicit color to .login_notice (#94A3B8) for 'Hello, username'
- Add explicit color to .cr_notice (#64748B) for copyright text

Section title improvements:
- Restore subtle gradient background (rgba(148, 163, 184, 0.15))
- Brighten title text color (#94A3B8) - visible but not pure white
- Improve hover state (#CBD5E1)

Consistent hover effects:
- Make .sidebar-contest and .sidebar-item links display:block
- Add padding, border-radius, and hover background to match folder headers
- Make .sidebar-action links consistent with same treatment
- Full row is now clickable with subtle hover highlight

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

* Further UI improvements

* Fix flash of expanded menus and simplify sidebar JS

- Add pre-paint CSS injection in <head> to apply collapsed state before render
- Refactor JS: abstract localStorage helpers into SidebarState object
- Use event delegation instead of inline onclick handlers
- Reduce code duplication by consolidating section/folder toggle logic

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

* Fix expand/collapse by using hydration class for pre-paint CSS

The pre-paint CSS injection was using attribute selectors that didn't
depend on the .collapsed class, so toggling the class had no visual
effect. Now the pre-paint CSS is gated by html.sidebar-hydrating class
which is removed after applySidebarState() runs, allowing the class-based
CSS to take over for interactive toggling.

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

* Add spacing between login notice and Administration title

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

* Add training program hierarchy to sidebar

- Add training program / training day breadcrumb when viewing a training day
- Add Training Programs section to sidebar with collapsible training days
- Update pre-paint CSS to support training-programs section key

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

* Fix missing training program menu in sidebar

- Add training program menu (General, Ranking, Submissions, Students, Tasks, Training Days, Announcements, Questions) when viewing a training program
- Update header to show training program name in breadcrumb
- Fix conditional logic to properly detect training_program context

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

* Fix sidebar issues: restore training_day condition, unanswered_questions_tp ID, and move Training Programs above Contests

- Restore {% if contest.training_day is none %} condition for Users menu item
- Restore unanswered_questions_tp ID for training program questions (was incorrectly changed to unanswered_questions)
- Move Training Programs section above Contests in sidebar as requested

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

* Fix duplicate password validation code in AddUserHandler

Remove duplicate block of password validation and get_password call that was
accidentally introduced during merge conflict resolution.

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

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Daniel Weber <[email protected]>

* Fix training day task removal warning, submissions handler, and ranking handler

- Add warning when removing a task from training program if it's also in a training day
- Create RemoveTrainingProgramTaskHandler with confirmation page showing training day name
- Remove task from both training day and training program when confirmed
- Update ContestSubmissionsHandler to query tasks by training_day_id for training day contests
- Update ContestUserTestsHandler similarly for consistency
- Change self.contest.tasks to self.contest.get_tasks() in RankingHandler for training day support

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

* Hide training day contests from contests list and sidebar

Training day contests now appear only under their training programs,
not in the main contests list or sidebar navigation.

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

* 📝 CodeRabbit Chat: Add unit tests for TrainingProgram, TrainingDay, and Student models

* Fix review comments - add training day delete in contest delete style

* Add start and end time fields to training day form with date pickers and sortable table

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

* Add start/end dates to training programs with validation and smart training day defaults

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

* Fix constraint violation for training day defaults and hide task options when 0 tasks

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

* Hide hidden contests from remove handler

* Address review comments: None checks, conditional warning text, client-side validation, and consolidated imports

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

* Refactor remove pages and date validation to use shared JavaScript helpers

- Add CMS.AWSUtils.initRemovePage() helper for task handling options on remove pages
- Add CMS.AWSUtils.initDateTimeValidation() helper for start/end time validation
- Update contest_remove.html, training_program_remove.html, training_day_remove.html to use shared helper
- Update add_training_program.html, add_training_day.html, training_program.html to use shared validation
- Fix training program removal to move tasks to other contests (not training programs)
- Change target dropdown from 'other training programs' to 'other contests' for consistency

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

* Add upcoming training days section to CWS overview page

- Display upcoming training days with start dates and countdown timers
- Show 'Started' indicator for active training days (actual_phase == 0)
- Hide training days with actual_phase >= 1 from overview
- Sort upcoming training days by proximity to start time
- Add JavaScript countdown that updates every second

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

* Fix timezone bug and redesign training day cards

- Fix countdown timezone: use server timestamp instead of client UTC time
- Redesign cards to match reference: cleaner layout with left border accent
- Add separate countdown boxes for days/hours/minutes/seconds
- Always show 'Enter Training' button (not just when started)
- Add duration display to training day cards
- Remove colorful gradients for cleaner look

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

* Improve design

* Address review comments + fix duration bug

* Hide training programs from browse view and add auto-login for training days

- Remove training programs from the contest browse view (folder_browse.html)
  so contestants access them directly via URL instead of seeing them in the list
- Add automatic authentication for training day contests: when a user is
  logged into a training program's managing contest, they are automatically
  authenticated to all training day contests within that program
- Clean up unused TrainingProgram import and training_program_href function

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

* Fix: Hide training day contests (not training programs) from browse view

- Restore training programs display in browse view (they should be shown)
- Add filter to exclude training day contests from browse view
  (contests where training_day is not null)
- Update both _build_folder_tree() and get() methods to exclude training days
- Keep auto-login feature for training days

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

* Change training day deletion to always keep tasks in training program

- Change Task.training_day_id FK from ON DELETE CASCADE to ON DELETE SET NULL
  so tasks are not deleted when a training day is deleted
- Update RemoveTrainingDayHandler to always detach tasks (no prompt for action)
- Update training_day_remove.html template to remove task options section
- Bump database version to 50

When a training day is deleted, its tasks now always remain in the training
program but are no longer assigned to any training day.

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

* Add database updater for version 50

Add update_50.py for the Task.training_day_id FK change from
ON DELETE CASCADE to ON DELETE SET NULL.

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

* Fix SQL migration to use ON DELETE SET NULL for tasks_training_day_id_fkey

Update the FK constraint in update_from_1.5.sql to match the model change
from ON DELETE CASCADE to ON DELETE SET NULL.

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

* Add delay request button for training days with configurable allow_delay_requests (#83)

* Add delay request button for training days in training program overview

- Create shared macro for delay request form (macro/delay_request.html)
- Refactor communication.html to use the shared macro
- Add 'Request Delay' button above 'Enter Training' button for each training day
- Button opens a modal with the delay request form
- Form submits to the training day's contest delay_request endpoint

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

* Add rejection reason field to delay requests (#82)

* Add rejection reason field to delay requests

- Add rejection_reason field to DelayRequest database model
- Update admin UI to allow entering optional rejection reason when rejecting
- Display rejection reason in admin UI for rejected requests
- Display rejection reason to users in communication page
- Add database migration for the new field

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

* Move textbox after button

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Add configurable delay requests and redirect support

- Add allow_delay_requests field to Contest model (default True)
- Add checkbox to admin contest form for allow_delay_requests
- Update DelayRequestHandler to enforce allow_delay_requests setting
- Add next parameter support for redirect after delay request submission
- Update communication.html to conditionally show delay request section
- Update training_program_overview.html to conditionally show delay request button
- Update delay_request macro to support next_url parameter
- Set allow_delay_requests=False by default for training program managing contests
- Add database migration for allow_delay_requests field

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

* Fix redirect after delay request and rename button

- Fix redirect not working: use absolute URL (/training_program_name/training_overview) instead of relative URL
- Rename button from 'Request Delay' to 'Request a Delay'
- Strengthen URL validation to also reject scheme-relative URLs (//)
- Remove leftover merge conflict marker from SQL migration file

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

* Add View Requests modal to training day cards

- Add 'View Requests (N)' button that shows when user has delay requests for a training day
- Add modal to display the delay request list using the existing delay_request_list macro
- Pass td_participation to template to access delay_requests relationship
- Button only appears when there are delay requests to view

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

* address review

* Move Enter Training button above delay request and fix button sizing

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

* Fix button sizing: Request a Delay + View Requests = Enter Training width

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

* Fix width

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* Add Tagify library for student tags UI with confirmation dialog for removal

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

* Add tag autocomplete, automatic 'student' tag, and task visibility tags

- Student tags: Add autocomplete suggestions from existing tags in the training program
- Student tags: Make 'student' tag automatic and irremovable for all students
- Student tags: Prevent duplicate tags
- Task visibility: Add visible_to_tags field to Task model
- Task visibility: Add UI for editing task visibility tags (restricted to existing student tags only)
- Task visibility: Add SQL migration for visible_to_tags column

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

* Show enter button only when training is soon, address nits

* Refactor tagify inputs to use shared utility and add training program column to tasks list

- Add CMS.AWSUtils.initTagify() shared utility function in aws_utils.js
- Refactor student tags (student.html) to use shared tagify utility
- Refactor task visibility tags (contest_tasks.html) to use shared tagify utility
- Fix delete confirmation to not trigger on automatic duplicate tag removal
- Add 'Training' column to Tasks list showing clickable link to training program

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

* Fix duplicate tag confirmation, move Training Day column, simplify code

- Fix duplicate tag confirmation: only show confirmation for user-initiated removals (click on X button), not for automatic removals like duplicate detection
- Move Training Day column from main tasks list to training_program_tasks.html
- Link to training day instead of training program
- Simplify config assignment code in initTagify (use ternary and direct assignment)

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

* Fix backspace removal confirmation and training day links

- Use timestamp-based approach to detect user-initiated removals (X click or Backspace/Delete key)
- Automatic removals (like duplicate detection) no longer trigger confirmation
- Fix training day links to go to the training day's contest page instead of training day admin page

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

* Add confirmAdd option for tag add confirmation

- Add confirmAdd option to initTagify utility that shows confirmation before adding a tag
- Uses 'add' event to show confirmation and immediately removes tag if user cancels
- Suppresses autosave during the undo removal to avoid unnecessary server requests
- Enable confirmAdd for student tags in student.html

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

* Refactor tagify to save only on confirmed changes

- Remove confirmAdd/confirmEdit/confirmRemove parameters - confirmation is now default
- Remove tagify.on('change') auto-save mechanism
- Call saveTags() explicitly after confirmed add, edit, and remove operations
- Automatic removals (duplicates) still save without confirmation
- Fix duplicate rejection_reason in SQL updater

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

* Fix tagify save mechanism to only save on confirmed changes

Key fixes:
- Use pendingSave flag with 'change' event instead of saving directly in event handlers
  (ensures input.value is updated before saving)
- Fix removeTag API call: use tagify.removeTags(tag, true) instead of
  tagify.removeTag(tag, { skipHook: true }) which was incorrect API usage
- Add isRollback flag to prevent confirmation prompts when rolling back cancelled adds
- Add armed flag to prevent confirmations during initial page load
- Save on 'change' event which fires AFTER Tagify updates input.value

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

* Fix cancelled add tags being saved on subsequent confirms

Key fixes:
- Change saveTags() to use tagify.value (canonical state) instead of input.value
  which may be stale if Tagify's debounced update() hasn't run yet
- Use non-silent removal for cancelled adds so Tagify properly updates internal state
- The isRollback flag still prevents confirmation dialog during rollback

This fixes the bug where cancelling an add would leave the tag in input.value,
causing it to be saved when a subsequent add was confirmed.

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

* Fix training day ranking

* Add main groups configuration for training days

- Add TrainingDayGroup model for per-group configuration
- Each group can have custom start/end times and task display order
- Students must have exactly one main group tag to participate
- Add admin UI for configuring main groups on training days
- Add GIN index on student_tags for efficient querying
- Implement per-group timing in CWS
- Implement per-group task ordering (custom vs alphabetical)
- Add ineligible students display in admin UI
- Bump database version to 50

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

* Fix delays_and_extra_times page: tags, ineligible students, phase management

1. Fix edit tags: Added inline editable tagify field for student tags in
   ineligible students section, reusing existing tagify implementation.
   Added shared initReadOnlyTagify function to aws_utils.js for read-only
   tags display, used in both training_program_students.html and
   delays_and_extra_times.html.

2. Ineligible Students section is now positioned right after All Participations.

3. Phase management improvements:
   - Added get_participation_main_group helper to find main group for each participation
   - Updated compute_actual_phase to accept optional main_group_start/end times
   - For users with no delay, planned start time now shows main group's start time
   - For users with delay, planned start time shows contest start + delay
   - Added warning in delay requests when requested start time is earlier than
     the student's group start time

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

* Fix get_participation_main_group to match by user_id instead of participation_id

Student.participation_id refers to the managing contest participation,
not the training day contest participation. Match by user_id instead
to correctly find the student's main group for training day contests.

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

* Fix bugs and refactor duplicated code in PR #55

Bug fixes:
- Fix Bug 1: Rename 'training_program' to 'ineligible_training_program' in
  DelaysAndExtraTimesHandler to avoid conflicting with base.html sidebar logic.
  This ensures the contest sidebar is shown for training day contests instead
  of the training program sidebar.
- Fix Bug 2: Pass effective_start and effective_stop to CWS overview template
  so that contest times account for main group timing in training programs.

Refactoring:
- Add shared get_all_student_tags() utility function in cms/server/util.py
- Refactor contestdelayrequest.py, contesttask.py, and trainingprogram.py
  to use the shared utility instead of duplicating tag collection logic

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

* Fix issues from bot review: imports, race condition, N+1 query, nested form, None handling

- Add tornado.web import in admin/handlers/contest.py for HTTPError
- Fix MOVE_BOTTOM race condition in contesttask.py by computing target index before DB modifications
- Add selectinload for training_program_list query in base.py to fix N+1 issue
- Convert nested form to AJAX POST in training_day_groups.html
- Add TrainingDayGroup import in contest/handlers/contest.py
- Handle None for student.student_tags in util.py

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

* coderabbit nits

* review comments

* Address bot review feedback: visibility filtering, error handling, accessibility, and security fixes

- taskusertest.py: Add visibility filtering for tasks, use self.can_access_task() method
- contesttask.py: Capture original tags before modifying to avoid stale values in error response
- task.py: Replace RuntimeError with tornado.web.HTTPError(500) and add logging
- trainingprogram.py: Normalize tags with lowercase, handle None stop date safely
- aws_style.css: Update Tagify styles and improve contrast for WCAG AA compliance
- aws_utils.js: Replace window.location.href with ajax_delete, add namespace
- delayrequest.py: Reject backslashes in URL validation to prevent open redirects
- training_program_overview.html: Header fallback, accessible close buttons, null guards in updateCountdowns, dedupe CSS

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: [email protected] <[email protected]>
Co-authored-by: Daniel Weber <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…on (#86)

* Refactor training day submissions to use managing contest participation

This refactoring changes how submissions are handled in training programs:

1. Add training_day_id field to Submission model to track which training day
   a submission was made via (or None for task archive/regular contest submissions)

2. Update SubmitHandler to use managing contest participation for training day
   submissions instead of training day participation, while recording the
   training_day_id on the submission

3. Update score cache logic to update both managing contest participation cache
   AND training day participation cache when a training day submission is scored

4. Update CWS TaskSubmissionsHandler to:
   - Filter submissions by training_day_id for training day context
   - Group submissions by source (task archive vs training day) for task archive view

5. Update CWS task_submissions.html template to display submissions grouped by
   source in task archive view

6. Update AWS ContestSubmissionsHandler to:
   - Filter submissions by training_day_id for training day contests
   - Pass is_training_program flag to template

7. Update AWS submission macro and contest_submissions.html to show a 'Source'
   column for training program submissions indicating which training day the
   submission came from (or 'Task Archive')

8. Add database migration (update_from_1.5.sql and update_50.py) to add
   training_day_id column to submissions table

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

* Move training day cache logic to ScoringService callers

Per code review feedback, moved the training day participation cache
handling from scorecache.py to the callers in ScoringService.py:

1. In ScoringExecutor.execute: After update_score_cache, if the submission
   has a training_day_id, invalidate the training day participation's cache
   so it gets rebuilt with only training day submissions.

2. In invalidate_submission: When collecting affected (participation, task)
   pairs, also add the training day participation pairs for submissions that
   have training_day_id set.

This keeps scorecache.py focused on its core responsibility and makes the
training day logic explicit at the call sites.

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

* Fix is_training_program undefined error in training program submissions

The TrainingProgramSubmissionsHandler was rendering contest_submissions.html
without passing is_training_program, which is needed by the macro to show
the training day source column.

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

* Fix download submissions for training days and training programs

1. DownloadContestSubmissionsHandler: For training day contests, now filters
   by training_day_id to only download submissions made via that training day.

2. DownloadUserContestSubmissionsHandler: Same fix for per-user downloads.

3. New DownloadTrainingProgramSubmissionsHandler: Downloads all submissions
   for a training program with folder structure: user/task/source/official/
   where source is either 'task_archive' or the training day description.

4. New DownloadTrainingProgramStudentSubmissionsHandler: Same for per-student
   downloads in a training program.

5. Updated templates to use the correct download URLs for training programs.

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

* Fix review comments

* Address CodeRabbit review comments

1. Pass training_program to template in ContestSubmissionsHandler so the
   download link routes correctly for managing contest submissions.

2. Extract _get_training_day_participation helper function to de-duplicate
   the training day participation lookup pattern.

3. Add memoization in invalidate_submission to avoid N queries during
   bulk invalidations when multiple submissions share the same
   (training_day_contest_id, user_id) pair.

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

* Address additional CodeRabbit review feedback

1. Use is_training_program|default(false) instead of 'training_program is defined'
   in contest_submissions.html template for consistent download URL routing.

2. Add joinedload(Submission.training_day) to queries in tasksubmission.py
   to avoid N+1 queries when accessing training_day relationship.

3. Add sanitize_path_component() function in submissiondownload.py to sanitize
   contest descriptions for safe use in zip file paths, preventing path
   traversal issues from special characters like / \ : * ? etc.

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

* Address CodeRabbit review: fix training-day context helpers and UI

- Extract _get_managing_participation helper to avoid code duplication
- Use managing_participation for task_score, tokens_available, and
  get_submission_count in training-day context (fixes incorrect scores,
  token availability, and submission counts)
- Add order_by(Submission.timestamp.desc()) to submission queries for
  stable UI ordering
- Always show 'Official submissions' header in task_submissions.html
  template (not just when unofficial submissions exist)

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

* Fix scorecache for training day participations

_get_sorted_official_submissions now correctly handles training day
participations by querying submissions from the managing contest's
participation (where they are stored) and filtering by training_day_id.

This fixes the issue where score cache rebuilds for training day
participations would return no submissions because they were looking
at the wrong participation.

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

* Extract get_managing_participation helper and fix GET/POST consistency

- Move get_managing_participation to cms/db/training_day.py as shared helper
- Update tasksubmission.py and scorecache.py to use the shared helper
- Fix GET/POST inconsistency: TaskSubmissionsHandler.get() now raises 403
  when managing participation is missing, consistent with SubmitHandler.post()

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

* Fix submission fetchers

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
* Add StudentTask model for task archive feature

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]>

* Add student_tasks to update_task spec in importing.py

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]>

* Address PR #87 feedback: sidebar visibility, access control, score optimization, 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]>

* Fix TypeError: StudentTask foreign keys must be set as attributes

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]>

* Add task archive progress column to students page

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]>

* Refactor: extract shared task archive progress calculation

- 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]>

* Update contest handler to use shared calculate_task_archive_progress 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]>

* coderabbit nits

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
* Add archive training day feature

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]>

* Store all student tags as array instead of single tag

- 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]>

* Fix schema_diff_test: remove DEFAULT clause from student_tags

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]>

* Fix IP detection to use starting_ip_addresses instead of participation.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 bugs and add start_time to archived training days

- 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 attribute error and sidebar training days

- 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]>

* Improve attendance table UI with badges and fix date filtering

- 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]>

* improve GUI

* Add combined ranking page for archived training days

- 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]>

* small bug fixes

* Fix archiving to use training_day.tasks and task_score function

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]>

* Use get_cached_score_entry instead of task_score for archiving

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]>

* Improve UI

* Improve archive training day: store all visible tasks, submissions, and 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]>

* Fix missing ARRAY import in archived_student_ranking.py

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

* Archive visible tasks based on student tags, not just existing StudentTask 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]>

* Add StudentTask records during archiving for visible tasks

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]>

* Fix graph scale and submission time display in combined ranking history

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]>

* Improve UI

* Share date filtering logic

* Fix submission fetching and handle archived training days

1. Fix submission fetching to use managing_participation.id, training_day_id,
   and official=True filter when archiving
2. Fix submission time offset to be relative to student start time
3. Add duration field to TrainingDay model and calculate at archiving time
   (max of main groups duration or training day duration)
4. Handle archived training days (where contest is None) in all places:
   - ScoringService: already checks contest_id is not None
   - Templates: show text instead of links for archived training days
   - Handlers: skip operations for archived training days
   - Submission download: use stored description/name for archived training days

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

* Small fixes

* Update cms/server/admin/static/aws_style.css

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix unstable task sorting by saving and using training_day_num

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

* Add global score column

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Add Training Days page to contest server with scores and sorting

- Add 'Training Days' menu item to contest server training programs
- Create new TrainingDaysHandler and training_days.html template
- Show ongoing/upcoming trainings section with countdown timers
- Show past trainings with training score, home score, and total score
- Add training score and home score columns to admin archive table
- Add assigned date and training source columns to task archive table
- Add sorting capability for assigned date and source columns

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

* styling

* Remove Total column and fix ended trainings display

- Remove Total column from past trainings table (same as home score)
- Add has_ended flag to handler for trainings with actual_phase >= 1
- Show red stripe and 'ENDED' label for ended trainings
- Change LIVE pill to green color for active trainings

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

* Fix task links in past trainings to use training program URL

Changed task links from contest_url (managing contest) to use the
training program's URL pattern: /<training_program>/tasks/<task>/description

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

* Address PR feedback: revert task link, fix aria-valuenow, optimize N+1 queries

- Revert task link change (contest_url was correct)
- Fix aria-valuenow clamping in progress bar (0-100 range)
- Remove unused data-total-score attribute from training_days.html
- Optimize N+1 query in TrainingDaysHandler (batch ArchivedStudentRanking)
- Optimize N+1 query in StudentTasksHandler (batch ArchivedStudentRanking)

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

* Replace logout button with 'Back to Training Program' in training day pages

When inside a training day contest, show a 'Back to Training Program' button
instead of the logout button, since login is now via the training program.

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
* Fix training program sidebar issues

1. Show training day side menu when viewing a task in an active training day
   - Modified TaskHandler.get() to set self.contest to the training day's
     contest when the task has an active training day (not archived)

2. Fix Overview and Resource Usage links to stay within training program context
   - Changed the condition in base.html to link Overview to the training
     program's general page when in training program context
   - Resource usage still links to the main resource usage page

3. Add collapsible Training Days submenu in training program sidebar
   - Replaced the simple 'Training Days' link with a collapsible section
   - Shows all active training days with links to their contest pages
   - Includes 'Add a training day' button for admins with permission_all

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

* Fix Overview and Resource Usage links to point to managing contest pages

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

* Add managing contest configuration fields to training program general page

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

* Refactor contest configuration fields into reusable template fragment

- Create fragments/contest_config_fields.html with all contest configuration fields
- Update contest.html to use the shared fragment
- Update training_program.html to use the shared fragment
- Remove tasks and students sections from training program general page (they have their own sections)

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

* Add redirect handlers for training program overview and resourceslist URLs

When users navigate to /training_program/{id}/overview or
/training_program/{id}/resourceslist, these handlers redirect them to
the managing contest's overview and resourceslist pages respectively.

This fixes 404 errors that occur when users access these URLs directly
or from cached pages.

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

* Fix redirect loop for training program overview and resourceslist pages

Add /overview and /resourceslist to the list of paths that should not be
redirected from managing contest URLs. This prevents the infinite redirect
loop between /training_program/{id}/overview and /contest/{id}/overview.

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

* Use searchable-select for adding students to training program

Update the 'Add a new student' form to use the same TomSelect-based
searchable dropdown that is used elsewhere in the system (e.g., adding
users to contests, adding tasks).

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

* Add searchable dropdowns and show all tasks for training day selection

- Add searchable-select to 'Select a task to add to this student's task archive'
- Add searchable-select to 'Select a task to assign to students'
- Update training day task selection to show all available tasks
- Tasks not in the training program are marked with '(not in training program)'
- Show confirmation dialog when selecting a task not in the training program
- Automatically add the task to the training program when confirmed

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

* Update cms/server/admin/templates/fragments/contest_config_fields.html

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Persist contest config fields in TrainingProgramHandler.post()

Add handling for the following fields that were rendered by
contest_config_fields.html but not being persisted:
- allowed_localizations (comma-separated list)
- allow_delay_requests (boolean)
- allow_unofficial_submission_before_analysis_mode (boolean)
- block_hidden_participations (boolean)
- allow_password_authentication (boolean)
- allow_registration (boolean)
- ip_restriction (boolean)
- ip_autologin (boolean)
- per_user_time (timedelta in seconds)
- min_submission_interval_grace_period (timedelta in seconds)

These fields are now read from the request and applied to the
managing_contest using the same patterns as ContestHandler.post().

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

* Fix multiple training program issues

- Auto-set starting_time when adding student to training program
  (so they can see everything immediately without a start button)
- Auto-set starting_time on registration to managing contest
  (training programs don't have a start button)
- Add 'training-days' to validSectionKeys in base.html to prevent
  flash of expanded menu on page load
- Fix BulkAssignTaskHandler.post to reject empty string task_id
  (in addition to 'null')
- Add rel='noopener noreferrer' to documentation link in
  contest_config_fields.html to prevent tabnabbing

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

* Fix validation and modernize JavaScript

- Fix AddStudentTaskHandler to reject empty string task_id
- Fix AddTrainingProgramStudentHandler to check for empty string user_id
- Modernize indexOf to includes in contest_tasks.html

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

* Create Student on registration and improve user participations UI

- Create Student record when user registers to training program's managing contest
  (mirrors AddTrainingProgramStudentHandler behavior)
- Also add participations to all existing training days on registration
- Hide training day contests from 'Add a new participation' dropdown
- Hide managing contests (training programs) from 'Add a new participation' dropdown
- Separate training program participations into their own section in user page
- Hide training day participations from user page (managed via training program)
- Add confirmation dialog for removing regular participations
- Training program removal links to 'remove from training program' flow

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…95)

* Add visible_to_tags field to announcements for student tag filtering

This feature allows announcements to be visible only to selected student tags,
mirroring how tasks in training days are filtered by student tags.

Changes:
- Add visible_to_tags field to Announcement model with GIN index
- Update SQL migration to add the column to existing databases
- Update AddAnnouncementHandler and EditAnnouncementHandler to support tags
- Update TrainingProgramAnnouncementsHandler to support tags
- Add can_see_announcement helper function to filter announcements
- Update get_communications to filter announcements by student tags
- Update announcements.html template with tag input fields
- Update aws_utils.js to populate tags in edit form

If no tags are selected, the announcement is visible to all students.
If tags are set, only students with at least one matching tag can see it.

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

* Fix announcement visibility filtering and add tagify support

Fixes:
1. Fix can_see_announcement to handle both training days and training programs
   - For training day contests, use get_student_for_training_day to find student
   - For managing contests, find student directly by participation
   - Properly check student tags against announcement visible_to_tags

2. Add tagify box for selecting announcement visibility tags
   - Create ContestAnnouncementsHandler to pass all_student_tags for training days
   - Update TrainingProgramAnnouncementsHandler to pass all_student_tags
   - Update announcements.html template to use tagify with whitelist of tags
   - Support both training programs and training day contests

3. Show tag visibility controls for training day announcements
   - Update template conditions to check for is_training_day in addition to training_program
   - Display visible_to_tags for existing announcements in both contexts

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

* Fix announcement visibility filtering in communication page

The communication.html template was directly iterating over contest.announcements,
bypassing the can_see_announcement filtering logic. This fix:

1. Adds get_visible_announcements() method to ContestHandler that filters
   announcements using the existing can_see_announcement() function
2. Adds visible_announcements to render_params
3. Updates communication.html to use visible_announcements instead of
   contest.announcements

This ensures that students only see announcements that match their tags
in training programs and training days.

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

* Fix Tagify initialization for announcement visibility tags

The DOMContentLoaded event listener was redundant because the js_init block
is already executed inside the init() function which runs after DOM is ready.
This caused Tagify to not initialize because DOMContentLoaded had already fired.

Removed the wrapper to match the pattern used by task visibility tags in
contest_tasks.html.

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

* Share tag parsing

* Address CodeRabbit review feedback

1. Fix Tagify sync when editing announcements
   - Use Tagify API (removeAllTags/addTags) instead of setting input.value directly
   - This ensures the Tagify UI updates properly when editing existing announcements

2. Add id attributes to visibility tag inputs for accessibility
   - Add id='visible_to_tags' to the add form input
   - Add id='edit_visible_to_tags' to the edit form input
   - This fixes label/input linkage for screen readers

3. Fix unauthenticated view filtering for non-training contests
   - Non-training contests now show all announcements regardless of tags
   - Only training program/day contests apply visibility filtering when not logged in

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

* Disable tag editing

* nit

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
)

* Add training day types for categorization and filtering

- Add training_day_types field to TrainingDay model (ARRAY(Unicode))
- Add SQL migration for the new field with GIN index
- Add TrainingDayTypesHandler for AJAX tag updates
- Add get_all_training_day_types utility function
- Add Tagify-based UI for editing training day types
- Rename TrainingProgramDateFilterMixin to TrainingProgramFilterMixin
- Extend mixin to filter by training day types (AND logic)
- Add type filter UI to attendance and combined ranking pages

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

* Fix training_day_types default to use lambda instead of list

The list builtin was being passed as the value instead of being called.
Using lambda: [] ensures the callable is properly invoked.

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

* Fix callable defaults and enforce whitelist for training day types filter

- Fix base.py set_attrs to properly call callable defaults instead of
  passing the callable itself as the value
- Revert training_day_types default from lambda: [] back to list
- Add enforceWhitelist: true to attendance and combined ranking pages
  so users can only select from existing training day types

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

* Fix history link to pass training_day_types filter and show types in table headers

- Update history link in combined_ranking template to include training_day_types
- Update history_url construction in handler to include training_day_types
- Pass training_day_types to combined_ranking_detail template
- Add training types display under training date in both attendance and
  combined ranking table headers

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

* Remove sorting arrows from training day headers and enable Global Total sorting

- Add 'no-sort' class support to init_table_sort to skip headers
- Add data-sort-column attribute support for custom column indices
- Add no-sort class to training day header cells (colspan headers)
- Add data-sort-column='-1' to Global Total header for last column sorting

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

* Improve archive error handling and add attendance badges in combined ranking

- Fail with appropriate error if managing_participation is None during archive
- For students with starting_time None (missed training), still create archived
  ranking with 0 scores but fail if submissions or history exist
- Skip attendance creation for ineligible students (not in any main group)
- Add attendance badges in combined ranking total column:
  - Home icon for students who participated from home
  - X icon for students who missed the training

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

* Fix separation

* Address PR review comments: fix permissions, badges, URL encoding, and Tagify

- Disable type editing inputs for non-privileged admins in training_days.html
- Add missed badge rendering when scores exist in combined_ranking.html
- Use urllib.parse.urlencode for query params in trainingprogram.py
- Replace initTagify with basic Tagify for filter inputs (no AJAX save)
- URL-encode training_day_types in history link

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

* Add student tag filtering to combined ranking page

- Add _parse_student_tags_filter() method to TrainingProgramFilterMixin
- Support two filter modes: 'current' (by current student tags) and 'historical' (by tags during each training)
- Filter students based on selected tags and mode
- Omit task columns for tasks with no filtered student data
- Omit training days where no filtered students were eligible
- Handle mixed visibility scenario (student A in t1, student B in t2 with same tag)
- Update history handler to filter by student tags
- Update detail handler to compute relative ranks among filtered students
- Add student tags filter UI with mode selector to template
- Update history links to include student tag filters

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

* Fix table sorting and size

* Add student tag filtering to attendance and histogram features for tasks/training days

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

* small fixes

* Fix histogram issues: training day click, historical tag filtering, upward bars, median, new bins

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

* Fix histogram filtering and add dynamic bucket scaling based on max possible score

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

* Fix histogram issues: filter inaccessible scores, dynamic max score, and add histogram icon

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

* Fix total column overlap and histogram tag filter dropdown z-index

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

* Add filtered student tags information to combined_ranking_detail page

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

* Add training day types filter information to combined_ranking_detail page

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

* Address code review feedback: null-guards, CSS variable, NaN handling, query params, XSS prevention, and modern Clipboard API

- Add null-guard for student.student_tags in trainingprogram.py (3 locations)
- Replace hardcoded top:90px with CSS variable --ranking-header-top in aws_style.css
- Fix NaN handling in sort function to use Infinity so missing values sink to bottom
- Fix training_day_types query param to use comma-join instead of repeated keys
- Guard against division by zero in histogram calculation
- Escape task names with tojson filter to prevent XSS in onclick handlers
- Update clipboard copy to use modern Clipboard API with fallback

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

* Reduce code duplication and fix minor bugs

* Fix JS bug

* tojson

* nits

* Fix sorting issues

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
…oup ranking split, and UI fixes (#101)

* Add timezone support, duration inputs, and hidden user filtering for training days

1. Change group times and training day inputs to use contest timezone instead of UTC
   - AddTrainingDayHandler now passes timezone_name to template
   - ContestHandler passes timezone info for training_day_groups fragment
   - All datetime inputs now display and parse in contest timezone

2. Update forms to use start time + duration instead of end date
   - AddTrainingDayHandler.post() parses duration_hours and duration_minutes
   - AddTrainingDayGroupHandler.post() uses duration instead of end_time
   - UpdateTrainingDayGroupsHandler.post() uses duration instead of end_time
   - Templates updated to show duration inputs (hours + minutes)

3. Hide hidden users from combined ranking and attendance
   - TrainingProgramAttendanceHandler filters out hidden students
   - TrainingProgramCombinedRankingHandler filters out hidden students
   - AddTrainingDayHandler passes hidden property when creating participations

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

* Remove redundant timezone info - already provided by BaseHandler.render_params()

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

* Add option to apply hidden status changes to existing training days

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

* Split training day ranking by main groups with separate tables and exports

- Add main group detection for training day contests
- Build student tags lookup for each participation
- Organize participations by main group based on student tags
- Filter tasks per group based on accessibility
- Add student tags column after user column in ranking table
- Create separate tables per main group with group headers
- Add per-group CSV/TXT export links with group name in filename
- Calculate group-specific total scores for filtered tasks
- Preserve original single-table behavior for non-training-day contests

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

* Fix tag retrieval, add tags to ranking exports, and fix remove main group button

- Fix tag retrieval in training day ranking: use user_id join instead of
  participation_id since Student.participation_id refers to managing contest
  participation, not training day participation
- Add student tags column to training program ranking tables and CSV export
- Add tags column to ranking.txt export template
- Fix remove main group button by using cookie-based XSRF token instead of
  handler.xsrf_token which returns bytes

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

* Fix main_groups_data undefined error in training program ranking

Add main_groups_data = None to TrainingProgramRankingHandler r_params
to prevent UndefinedError when rendering ranking.html template

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

* Redesign group header styling and add inaccessible indicator to exports

- Replace gradient header with simple border-bottom style matching page design
- Use neutral colors for export links instead of white-on-gradient
- Add 'N' indicator for inaccessible tasks in TXT export
- Add 'N/A' indicator for inaccessible tasks in CSV export (trainingprogram.py)

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

* Move export links next to group title and fix table sorting

- Remove border-bottom from group header, use inline layout with gap
- Place CSV/TXT links directly after group name
- Use loop.index for table IDs to avoid special character issues in jQuery selectors

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

* Fix task score sorting by adding data-value attribute

The sorting function uses parseFloat on cell text, but task cells contain
emoji indicators that make parsing fail. Adding data-value with the numeric
score allows the sorting function to use the correct value.

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

* Fix table styling by using td-ranking-table class

Rename ranking-table class to td-ranking-table to avoid picking up
the combined ranking page's CSS styling which changes header appearance.

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

* Fix 7 UI issues in training program/day interface

1. Add 'participation' column in Training Program Participations table in user page
2. Link users to training day participation pages in submissions page
3. Link user in training day participation to students page instead of users page
4. Fix submission list in user participation of training days to show correct submissions
5. Fix 404 error when clicking 'history' in training program ranking by using get_tasks()
6. Fix history link in training day ranking by using get_tasks() consistently
7. Fix profile picture not loading in combined ranking user details

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

* Add source column to submission table in student page

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

* Fix submission display in training day participation page

For training day participations, submissions are stored with the managing
contest's participation, not the training day's participation. This fix
uses get_managing_participation to find the correct participation and
filters submissions by training_day_id.

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

* Fix training program ranking history link 404

Add TrainingProgramStudentDetailRedirectHandler to handle
/training_program/{id}/student/{user_id}/detail URLs by redirecting
to the managing contest's participation detail page.

Update ranking.html template to use the correct URL pattern for
training program ranking history links.

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

* Fix 7 code issues and refactor trainingprogram.py into modules

Code fixes:
- contestranking.py: Import get_all_student_tags from cms.server.util
- contestranking.py: Sort by group-specific total using accessible_tasks
- contestranking.py: Use deterministic main group selection and batch query
- trainingday.py: Add duration validation for hours/minutes bounds
- ranking.html: Use contest.score_precision for total formatting
- ranking.txt: Use contest.get_tasks() instead of contest.tasks
- training_program_combined_ranking_detail.html: Handle missing face image

Refactoring:
- Split trainingprogram.py (3306 lines) into logical modules:
  - trainingday.py: Training day management handlers
  - student.py: Student management handlers
  - archive.py: Archive, attendance, and combined ranking handlers
  - trainingprogram.py: Core training program handlers (965 lines)
- Update __init__.py to import from new modules

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

* Fix training day time validation and combined ranking error

- Add bidirectional validation in ContestHandler.post() to ensure training day
  times don't conflict with main group times (start can't be after group start,
  end can't be before group end)
- Fix combined ranking template error by passing ArchivedStudentRanking objects
  instead of float scores in ranking_data dict

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

* Add archive training button on attendance page and warning on archive confirmation

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

* Restore missing training_day_tasks and other variables in TrainingProgramCombinedRankingHandler

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

* Add validation, fix N+1 queries, and code improvements

- Add shared parse_and_validate_duration() function in trainingday.py
- Add task ownership validation in student.py (single and bulk assignment)
- Add error handling for malformed main_group_user_ids parameter
- Fix N+1 query in trainingprogram.py student tag lookup (batch query)
- Fix N+1 query in contestranking.py student tag lookup (batch query)
- Move task_index computation outside group loop in ranking.html
- Fix loop variable capture in archive.py get_training_day_num()

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

* Update removeMainGroup() to use hidden input pattern for XSRF token

Use document.querySelector('input[name="_xsrf"]') instead of getCookie()
for consistency with other templates in the codebase.

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

* PR review comments

* Refactor datetime parsing to use get_datetime_with_timezone and add validation

- Refactor trainingday.py to use parse_datetime_with_timezone instead of manual UTC parsing
- Update parse_datetime_with_timezone to handle HTML5 datetime-local format (YYYY-MM-DDTHH:MM)
- Add validation to prevent 0-duration training and group times when inputs are provided
- Fix XSRF token handling in training_day_groups.html with explicit null check
- Fix GET side effect in student.py by removing auto-creation of Student record

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

* share logic, fix contest stop bug

* Fix checkbox re-indexing issue when rows are deleted

Re-index checkbox values before form submission to match visual order,
fixing the issue where deleting rows caused checkbox values to mismatch
with the enumerate index in the Python backend.

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

* nits

* fix table headers

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
* Improve notifications for training day attendance and questions

- Add clear messages on announcements/questions pages indicating they only
  handle training program items, with links to training day pages
- Add alerts to side menu showing total unanswered questions and pending
  delay requests across all training days
- Add red circles with counts to each training day in the side menu
- Training program questions page now shows alerts for training days with
  unanswered questions with direct links
- Training program attendance page now shows alerts for training days with
  pending delay requests with direct links
- Add orange color styling for delay request indicators to differentiate
  from question indicators (red)

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

* Consolidate training program render params into helper method

- Refactor render_params_for_training_program() to include contest, unanswered
  questions count, and training day notifications in a single helper method
- Update all training program handlers to use the consolidated helper method
- Remove duplicate code for setting training_program, contest, unanswered params
- Remove redundant Attendance badge from sidebar (now shown in Training Days)
- Fix CSS layout so notification badges stay inline with training day names

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

* Add training day notifications to overview and resourceslist pages

When the contest is a managing contest for a training program, use
render_params_for_training_program() to show training day notifications
in the sidebar on the overview and resourceslist pages.

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

* Notifications on right + new util useage

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…bmissions column (#103)

* Split unsolved tasks into attempted/not attempted and add sortable submissions column

- Task archive (CWS): Split 'unsolved' category into 'attempted' (has submissions)
  and 'not attempted' (no submissions) with separate counters and filter buttons
- Task archive (CWS): Add appropriate colors for each status (blue for attempted,
  gray for not attempted)
- Admin student tasks (AWS): Add submissions count column that links to the
  student's submission page for that task
- Admin student tasks (AWS): Make the table sortable by task name, assigned date,
  training score, home score, and submissions count
- Add submission_counts parameter to calculate_task_archive_progress utility

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

* Add inaccessible task indicator and rename sidebar link to Home Task Scores

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

* Fix: Mark inaccessible tasks on Home Task Scores page and rename sidebar link

- Modified TrainingProgramRankingHandler to check StudentTask records for task access
- Tasks not in a student's archive now show the 🚫 symbol on the ranking page
- Renamed sidebar link from 'Ranking' to 'Home Task Scores'
- Reverted incorrect changes to Combined Ranking page (kept original dash symbol)

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

* Fix submissions link and task archive progress calculation

- Add StudentTaskSubmissionsHandler for viewing submissions filtered by task
- Add route /training_program/{tp_id}/student/{user_id}/task/{task_id}/submissions
- Create student_task_submissions.html template
- Fix submissions column link in student_tasks.html to use new route
- Add get_student_archive_tasks() utility function for fetching tasks in student's archive
- Fix calculate_task_archive_progress() to use get_cached_score_entry for accurate scores
- Update callers to pass sql_session parameter for proper score cache usage

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

* Fix stale task_scores cache and add Task Archive Progress column

- Fix StudentTasksHandler to use get_cached_score_entry for home_scores
  instead of iterating over participation.task_scores (which may be stale)
- Fix _build_past_training_info to use get_cached_score_entry for home scores
- Add Task Archive Progress column to ranking.html for training programs
  (replaces Global column, only shown for training program managing contests)
- Make training_program_students.html table sortable with numeric sorting
  on Task Archive Progress column
- Add proper session commits to release advisory locks from cache rebuilds

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

* Share common logic

* Address review comments

* Redesign student tasks page with modern card-based UI

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

* Add bulk add students from file functionality for training programs

- Create shared utility parse_usernames_from_file in util.py
- Add BulkAddTrainingProgramStudentsHandler for bulk adding students
- Update BulkAddContestUsersHandler to use shared utility
- Add bulk add form and results display to training_program_students.html
- Register new handler and route in handlers/__init__.py

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

* Redesign students page with modern UI matching student_tasks design

- Move shared CSS from student_tasks.html to aws_style.css for reuse
- Add action buttons row with Import List, Bulk Assign Task, Add Student
- Add Task Archive Progress card with visual progress bars
- Modernize students table with sortable columns and consistent styling
- Update handler to support new remove button format

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

* Fix submission_count undefined error in student remove confirmation

The render_params_for_training_program method reinitializes r_params,
so it must be called before render_params_for_remove_confirmation
which adds submission_count to the existing r_params.

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

* Refine students page design based on feedback

- Move Task Archive Progress into table as column with small inline bar
- Combine username/first name/last name into single Student column
- Change Add Student button from purple to teal (training program theme)
- Rename 'Import List' to 'Add Students from List' for clarity
- Remove separate progress card section

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

* Cleanup students page: remove duplicate tags column, add score display, fix progress colors

- Remove duplicate Student Tags column (tags now shown only in Student column as badges)
- Remove tagify readonly initialization (no longer needed)
- Add score display (score/max) to Task Archive Progress column
- Fix progress bar colors: green >= 80%, orange >= 40%, red < 40%
- Update table to 3 columns: Student, Task Archive Progress, Actions

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

* Improve students table: profile pictures, inline tags, better sorting

- Add profile picture circle (or initials placeholder) in Student column
- Move tags inline after username instead of on separate row
- Update sorting so 'No tasks' students always appear at end when sorting by progress
- Narrow Student column by making layout more compact

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

* Fix initials placeholder error when name is empty

Use slice notation [:1] instead of [0] to safely handle empty names

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

* Improve table alignment and styling based on reviewer feedback

- Left-align both Student and Task Archive Progress columns (header and content)
- Narrow table width (auto width with min-width instead of 100%)
- Replace Remove button with trash icon SVG
- Ensure 0% progress bar shows gray track clearly (min 5% width for visibility)

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

* fix sorting

* Replace bulk assign task page with modal and centralize CSS styles

- Replace bulk_assign_task.html page with a modal dialog in training_program_students.html
- Update BulkAssignTaskHandler to redirect to students page after POST
- Remove GET method from BulkAssignTaskHandler (no longer needed)
- Delete bulk_assign_task.html template
- Create aws_tp_styles.css with CSS custom properties (color variables) for theming
- Move inline styles from training_program_students.html to centralized CSS
- Add aws_tp_styles.css to base.html for global availability

This improves maintainability and enables future theme customization.

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

* Centralize css

* Fix styling issues: student column layout, attendance headers, and move inline styles to CSS

- Fix student-info to use flex-wrap with proper truncation for names
- Restore center alignment for attendance and ranking table headers
- Add max-width to student-name-link for proper ellipsis truncation
- Move histogram modal styles from training_program_combined_ranking.html to aws_tp_styles.css
- Replace inline styles with CSS classes in combined ranking template
- Add combined-ranking-* CSS classes for student wrapper, info, name row, and tags

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

* Refactor user detail templates to share common code

- Create fragments/user_detail_styles.html for shared CSS styles
- Create fragments/user_detail_layout.html for shared HTML structure
- Create fragments/user_detail_scripts.html for shared JavaScript utilities
- Update participation_detail.html to use shared fragments
- Update training_program_combined_ranking_detail.html to use shared fragments
- Add initUserDetail() function with options for contest rows and AJAX submissions

This reduces code duplication from ~450 lines per file to ~80-100 lines each.

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

* small fixes

* Address coderabbit comments

* nits

* additional comments

* Update cms/server/admin/templates/fragments/user_detail_layout.html

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…days (#107)

* Redesign tasks page and add release/reuse functionality for archived training days

- Modernize the tasks page design to match the students page style
- Add modern table with sortable columns
- Add dropdown for adding tasks
- Add release/reuse button for tasks in archived training days
- Add remove button with confirmation for active training day tasks
- Add move buttons (top, up, down, bottom) for reordering tasks

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

* Rename to detach + style imp

* Add drag-and-drop reordering, consolidate CSS, fix remove behavior, add shared utility

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

* Use card-style add task UI and convert remove page to modal

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

* Fix task removal redirect, remove deprecated page, and modernize notification alerts

- Fix redirect after task removal to correctly return to tasks page
- Remove deprecated training_program_task_remove.html (now using modal)
- Remove unused GET handler from RemoveTrainingProgramTaskHandler
- Add modern info alert component (tp-info-alert) with warning/info variants
- Update training_program_attendance.html with modern alert styling
- Update questions.html with modern alert styling
- Update announcements.html with modern alert styling
- Add CSS styles for info alert badges with counts

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

* Add info note to combined ranking, redesign training days page with tasks column and histograms

- Add info alert to combined ranking page explaining it shows archived training days only
- Redesign training_days page with modern tp-* CSS classes
- Add Tasks column to both active and archived training days tables
- Active training days show task badges linking to task pages
- Archived training days show task badges with histogram icons
- Create reusable histogram_modal.html fragment for score distribution visualization
- Clicking archived task badges opens histogram modal with score distribution

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

* Fix training_days page - keep original styling, add tasks column with histogram support

- Reverted to original page styling (not the modern redesign)
- Added Tasks column to both active and archived training days tables
- Active training days show task badges linking to task pages
- Archived training days show task badges with histogram icons
- Removed duplicated histogram_modal.html fragment
- Use existing histogram CSS classes from aws_tp_styles.css
- Histogram modal opens when clicking on archived task badges

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

* Modernize training_days page with drag-and-drop, icons, and improved time display

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

* Add scoreboard sharing feature for archived training days

- Add scoreboard_sharing JSONB column to TrainingDay model (db version 49->50)
- Add admin UI to configure scoreboard sharing per archived training day
  - Modal to add/remove tags and set top_names count per tag
  - Visual indicator showing number of tags shared
- Add CWS handler to fetch scoreboard data with tag-based filtering
- Add CWS UI with scoreboard badges and modal
  - Students see badges only for tags they had during training
  - Scoreboard shows only students with matching tags
  - Scoreboard shows only tasks accessible to those tags
  - Top N students show full names, others anonymized
  - Current student's row is highlighted

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

* Fix: keep db version at 49, add schema migration for scoreboard_sharing

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

* share and fix common histogram logic

* UI improvements

* Modernize training_programs page with card-based layout

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

* Refine training_programs page: remove header button/subtitle, add indicators, modernize add form

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

* Address review comments

* Enhance scoreboard sharing with everyone option, top_to_show limit, and tie handling

- Add '__everyone__' option to share scoreboard with all students regardless of tags
- Add 'top_to_show' field to limit number of results displayed (with 'all' checkbox)
- Add 'top_names' field to limit how many top results show full names (with 'all' checkbox)
- Implement proper tie handling: students with same score get same rank
- Show all tied students at cutoff even if exceeds top_to_show limit
- Always show current user's rank even if past top_to_show limit
- Add validation caps based on actual student counts in admin UI
- Update admin modal with new fields and checkboxes
- Update CWS handler with new filtering and ranking logic

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

* UI improvements: modernize training cards, progress bar, add icons, and clickable training days

Contest server changes:
- Add clock and hourglass SVG icons to training day cards (start/duration)
- Redesign progress bar with dark navy background, percentage display, and score sections
- Add upload icon to Submit button
- Add grid icon to scoreboard buttons and display 'everyone' instead of '__everyone__'

Admin server changes:
- Modernize add_training_day.html with modern form styling (tp-form-card pattern)
- Make 'ACTIVE TRAINING DAYS' label clickable, linking to training_days page

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

* Fix UI feedback: input widths, select box, LIVE badge, duration icon, progress colors

- Admin: Reduce hours/minutes input width from 60px to 50px in Main Groups Configuration
- Admin: Replace Tagify with native select box in Scoreboard Sharing modal
- Admin: Update task archive progress bar thresholds (50% instead of 40%)
- Contest: Update LIVE badge to red pill with pulsing dot
- Contest: Fix duration icon to proper hourglass SVG
- Contest: Add conditional progress bar colors (red <50%, orange 50-80%, green >=80%)

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

* address review comments

* nits

* Address PR review comments: validation, defensive parsing, and code cleanup

- Extract shared table sorting utility in training_program_training_days.html
- Convert forEach to for loop with proper early exit in saveScoreboardSharing
- Validate reorder flow in trainingday.py - verify active td ids and positions
- Update task reorder validation to 0-based in trainingprogram.py
- Guard optional globals in histogram_js.html (tagsPerTrainingDay, etc.)
- Avoid duplicate operation input in training_program_tasks.html
- Fix sort key mismatch for training day column in training_program_tasks.html
- Null-check XSRF input in training_program_training_days.html
- Defensive parsing of top_to_show/top_names in contest/trainingprogram.py

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

* Address additional CodeRabbit review comments

- Guard allStudentTags in Tagify initialization to prevent ReferenceError
- Avoid duplicate operation input in saveNewOrder for training days
- Validate training_day_id safely with try/except in ScoreboardDataHandler
- Remove redundant import json inside function (already imported at module level)
- Skip confirmation when task order hasn't changed (UX improvement)
- Validate top_names <= top_to_show when both are integers

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

* Address additional CodeRabbit review comments

- Guard allStudentTags in Tagify initialization to prevent ReferenceError
- Avoid duplicate operation input in saveNewOrder for training days
- Validate training_day_id safely with try/except in ScoreboardDataHandler
- Remove redundant import json inside function (already imported at module level)
- Skip confirmation when task order hasn't changed (UX improvement)
- Validate top_names <= top_to_show when both are integers

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

* nits

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…o attendance (#109)

* Add justified absences, comments, and proctored status to attendance

- Add justified, comment, and proctored fields to ArchivedAttendance model
- Add UpdateAttendanceHandler for updating attendance records via AJAX
- Update attendance page with modal for editing justified/proctored/comment
- Show justified status with green checkmark, proctored with camera icon
- Add comment indicator icon when comments exist
- Update combined ranking page to show justified and proctored badges
- Add CSS styles for new badges

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

* Add default=False to justified and recorded columns

Without default=False, archiving training days fails with IntegrityError
because the ORM doesn't have default values for these NOT NULL columns.

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

* Fix client side updates

* Add Excel export feature for attendance page

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

* Improve Excel export: add training types, zebra colors, student tags, and filter-based filename

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

* Change student tags to separate column instead of split row

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

* Change student tags to separate column instead of split row

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

* Small comment display change

* Add Excel export for combined ranking page with shared utilities

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

* review comment

* review comments

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* Fix broken exports for training programs

1. Add 'Export all tasks' button to training program general page
   - Added export link that exports the managing contest's tasks

2. Fix training day export to use get_tasks() instead of contest.tasks
   - Training days have tasks separate from contest.tasks
   - Updated _export_contest_to_yaml_format to use get_tasks()

3. Rename managing contest to match training program name
   - Changed from '__' + name to just name
   - Updated contest filtering to check if contest is a managing contest
   - Updated exclude_internal_contests() to filter managing contests
   - Updated CWS and AWS handlers to check training_program relationship

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

* Fix export button URL and add training program export route

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

* review comments

* Add pictures to dump exporter

* export fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
* Add Hebrew translations for training program trans blocks

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

* fixes

* Update cms/locale/he/LC_MESSAGES/cms.po

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Add common training program utilities to reduce code duplication

- Add get_student_for_user_in_program() utility for common student lookup pattern
- Add get_student_tags_by_participation() for batch student tags queries
- Add count_unanswered_questions() and count_pending_delay_requests() utilities
- Add get_training_day_notifications() and get_all_training_day_notifications()
- Refactor admin/handlers/base.py to use new utilities
- Refactor admin/handlers/trainingprogram.py to use new utilities
- Refactor admin/handlers/archive.py to use new utilities
- Refactor admin/handlers/contestranking.py to use new utilities
- Refactor contest/handlers/trainingprogram.py to use new utilities

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

* Add shared validation helpers for user test and submission handlers

- Add get_validated_user_test() to ContestHandler for common user test validation
- Add get_validated_submission() to ContestHandler for common submission validation
- Refactor UserTestStatusHandler, UserTestDetailsHandler, UserTestIOHandler, UserTestFileHandler
- Refactor SubmissionStatusHandler, SubmissionDetailsHandler, UseTokenHandler

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

* Add get_training_day_timing_info utility and simplify code

- Add get_training_day_timing_info() to consolidate duplicated participation/eligibility logic
- Refactor TrainingProgramOverviewHandler and TrainingDaysHandler to use the new utility
- Simplify if/elif/elif to list comprehension with 'or' conditions
- Replace dict comprehension with dict() constructor

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

* Extract submission counts query into shared utility

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

* Refactor code duplications in templates and archive handlers

- Extract training day card HTML into shared macro (training_day_card.html)
- Extract countdown and table sorting JS into shared module (training_countdown.js)
- Update training_days.html and training_program_overview.html to use shared code
- Extract build_attendance_data function to reduce duplication in archive.py
- Extract build_ranking_data function for shared combined ranking logic
- Extract excel_write_training_day_header function for shared Excel header writing
- Update handlers to use new shared utility functions

Reduces ~140 lines of HTML/JS duplication and ~200 lines of Python duplication.

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

* nits

* Consolidate table sorting and Tagify filter utilities

- Remove table sorting from admin_table_utils.js, use CMS.AWSUtils.init_table_sort instead
- Update training_program_tasks.html to use CMS.AWSUtils.init_table_sort with data-value on cells
- Update training_program_training_days.html to use CMS.AWSUtils.init_table_sort
- Update training_program_students.html to use CMS.AWSUtils.init_table_sort
- Update student_tasks.html to use CMS.AWSUtils.init_table_sort
- Consolidate Tagify filter initialization in training_program_combined_ranking.html
- Consolidate Tagify filter initialization in training_program_attendance.html
- Add admin_table_utils.js to base.html for shared drag-drop and Tagify utilities

This reduces ~409 lines of duplicated JavaScript code by using the existing
CMS.AWSUtils.init_table_sort function instead of creating a parallel implementation.

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

* nits

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
* Refactor training program UI: CSS improvements and centralized JS

- Replace Jinja2 inline color calculations with CSS custom properties
  - Score cells now use CSS calc() with --score variable for hue calculation
  - Progress bars use CSS custom properties for dynamic width and color

- Replace custom dropdown with native HTML <dialog> element
  - Add student dialog uses native dialog for better accessibility
  - Bulk assign modal converted to native dialog element
  - Removed manual backdrop/escape key handling (native behavior)

- Create centralized training_program.js for histogram functionality
  - Move histogram modal logic from inline template to dedicated JS file
  - Templates now initialize CMS.TrainingProgram module with data
  - Maintains backwards compatibility with existing onclick handlers

- Add new CSS classes for dialog and progress bar components
  - .tp-dialog, .tp-dialog-header, .tp-dialog-body, .tp-dialog-footer
  - .progress-link, .progress-percentage, .progress-score
  - .cell-content.score-cell for CSS-based score coloring
  - .progress-bar-modern for CSS-based progress bars

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

* Fix duplicate HTML IDs and split aws_utils.js into modules

- Remove countdown timer from training program footer (keep only for contests)
- Fix duplicate 'main-groups-table' ID in training_day_groups.html by consolidating table structure
- Fix duplicate 'show-indicators' ID in ranking.html by using class selector
- Create aws_table_utils.js with table sorting/filtering functions
- Create aws_form_utils.js with form utilities (tagify, password, datetime, remove page)
- Update base.html to include new JS files
- Refactor training_program.js to use namespace pattern (CMS.TrainingProgram)

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

* Fix Add Student dialog size to accommodate dropdown

- Add overflow: visible to tp-dialog to allow dropdown to extend beyond dialog
- Add min-height to dialog body to ensure buttons are visible when dropdown is open

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

* Remove extracted functions from aws_utils.js (cleanup)

Functions moved to aws_table_utils.js:
- get_table_row_comparator
- sort_table
- init_table_sort
- filter_table

Functions moved to aws_form_utils.js:
- initPasswordStrength
- initDateTimeValidation
- initRemovePage
- initReadOnlyTagify
- initTagify

This completes the modularization of aws_utils.js, reducing it from
1728 lines to 1179 lines while maintaining backward compatibility
through aliases in the new utility files.

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

* cleanup css

* Remove unused histogram_js.html fragment

This file contained histogram modal JavaScript that was duplicated in
training_program.js during the refactoring. The histogram functionality
is now centralized in training_program.js, making this file obsolete.

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

* nits

* nits

* Update cms/server/admin/static/aws_table_utils.js

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
Co-authored-by: ronryv <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ng days

When a contestant reads a statement in a training day, also create a
StatementView for the training program's managing contest participation.
This ensures all statements read within a training program (either
directly from task archive or via training days) are tracked at the
training program level.

Co-Authored-By: Ron Ryvchin <[email protected]>
- Add setup_contest_or_training_program helper method to BaseHandler
- Consolidate TrainingProgramSubmissionsHandler with ContestSubmissionsHandler
- Consolidate TrainingProgramAnnouncementsHandler with ContestAnnouncementsHandler
- Consolidate TrainingProgramQuestionsHandler with QuestionsHandler
- Use (contest|training_program) URL pattern for shared routes
- Fix build_user_to_student_map usage in trainingprogramtask.py
- Remove duplicate handler classes and clean up unused imports

Co-Authored-By: Ron Ryvchin <[email protected]>
ronryv and others added 2 commits February 3, 2026 00:15
* Extract shared task data utilities and remove fallback logic

- Add TaskScoreInfo namedtuple and get_task_score_info() utility to extract
  max_score, extra_headers, and score_precision from a task's active dataset
- Add build_task_data_for_archive() for archiving task metadata
- Add build_task_data_for_detail_view() for detail view pages
- Add build_archived_tasks_data() to build complete archived_tasks_data dict

- Update ArchiveTrainingDayHandler to use build_archived_tasks_data()
- Update ParticipationDetailHandler to use build_task_data_for_detail_view()
- Remove fallback to live task data in TrainingProgramCombinedRankingDetailHandler
  and build_ranking_data() - archived training days should have all task data
  in archived_tasks_data; log warning if data is missing

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

* Improve code structure

* student.py: Use existing base class

* fix review comments

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ron Ryvchin <[email protected]>
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: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cms/server/admin/templates/macro/submission.html (1)

201-201: ⚠️ Potential issue | 🟡 Minor

Minor: Extra quote in id attribute.

There's a typo with an extra double-quote in id="latency"".

🐛 Fix typo
-  <td id="latency"">
+  <td id="latency">
🤖 Fix all issues with AI agents
In `@cms/server/admin/handlers/archive.py`:
- Around line 155-215: The post handler can leave the SQLAlchemy session dirty
on exception; in the except block inside post (method post, symbols:
self.sql_session, training_day, contest, self.try_commit) call
self.sql_session.rollback() before adding the failure notification and
redirecting so the session is reset, then return as before; ensure rollback
happens before any other session-using calls in that error path.

In `@cms/server/admin/handlers/contestdelayrequest.py`:
- Around line 49-76: get_participation_main_group duplicates eligibility logic
and does case-sensitive tag matching; replace its manual scan with a call into
the shared check_training_day_eligibility utility (or whichever canonical
function computes a student's main group) to ensure consistent, case-insensitive
rules and reuse any precomputed results. Locate get_participation_main_group and
replace the loop over training_program.students and the
main_group_tags/student_tags matching with a delegation to
check_training_day_eligibility (or the shared helper that returns the student's
eligible group/tag), passing the contest/training_day and participation.user_id
(or the student object) as required, and return the resulting TrainingDayGroup
if present. Ensure tag comparisons follow the shared function's case
normalization and remove the ad-hoc set/intersection logic so behavior remains
canonical and DRY.

In `@cms/server/admin/handlers/contestsubmission.py`:
- Line 28: The handler references Contest in ContestUserTestsHandler.get via
self.safe_get_item(Contest, contest_id) but Contest is not imported; update the
import statement that currently reads "from cms.db import Submission, UserTest,
Task" to also import Contest so the symbol is available to
ContestUserTestsHandler and avoid NameError at runtime.

In `@cms/server/admin/handlers/excel.py`:
- Around line 81-85: The excel_safe function only checks the very first
character and misses values where the first non-space character is a formula
token (e.g., " =1"). Update excel_safe to detect the first non-whitespace
character (use str.lstrip or iterate to find the index of the first non-space)
and, if that character is one of "=", "+", "-", "@", return the original string
prefixed with an apostrophe; ensure you handle empty or all-whitespace strings
by returning them unchanged. This change should be made inside the excel_safe
function so it preserves the original value except for the added leading
apostrophe when a formula token appears after any leading whitespace.

In `@cms/server/admin/handlers/submissiondownload.py`:
- Around line 295-304: The current query joins Task and filters Task.contest_id
== managing_contest.id which excludes training-day submissions (they use
Task.training_day_id); instead query Submission for the managing contest via the
submission's participation relationship so training-day submissions are
included. Update the submissions query (the block using Submission, Task,
joinedload(Submission.training_day)) to remove the join/filter on
Task.contest_id and replace it with a filter that matches the managing contest
on Submission.participation (e.g. Participation.contest_id ==
managing_contest.id), keeping the joinedload(Submission.training_day) and
allowing get_source_folder() to handle training-day submissions.

In `@cms/server/admin/handlers/training_analytics.py`:
- Around line 582-593: The handler currently writes an explicit error JSON on
try_commit() failure; remove the write_error_json(500, ...) call and instead
return early so centralized error handling/logging can handle the failure;
locate the block around try_commit() in training_analytics.py (the code that
calls self.try_commit(), self.write(...), and self.write_error_json(...)) and
replace the else branch with an early return (no explicit response), preserving
the successful self.write(...) branch and any local variables like
att.justified/att.comment/att.recorded.
- Around line 127-132: Remove the runtime filter that excludes archived students
based on current participation.hidden: in the loop over
archived_student_rankings (symbol: archived_student_rankings, variable: rank)
delete the check that inspects rank.student.participation.hidden so historical
visibility is preserved; likewise update get_student_detail_data to stop
filtering historical student lists by current participation.hidden (remove that
conditional), while leaving the access-control check that uses current hidden
status for detail pages unchanged.

In `@cms/server/admin/handlers/trainingprogram.py`:
- Around line 379-449: The loop that deletes training days assumes
training_day.contest is always present; if it's None (archived day) deleting
None will raise. In the delete method, update the loop over
training_program.training_days (and the local td_contest variable) to check for
a non-None contest before calling self.sql_session.delete(td_contest) — always
delete the training_day, but only delete td_contest when td_contest is truthy;
reference the delete method, training_program.training_days,
training_day.contest and td_contest to locate the change.

In `@cms/server/admin/handlers/trainingprogramtask.py`:
- Around line 290-334: Ensure the handler validates task ownership before
detaching it: in the delete method (the function decorated with
`@require_permission` and using safe_get_item for TrainingProgram and Task), check
that task.contest equals the training_program.managing_contest (or is
None/expected) and abort (raise a permission/validation error or return 404/403)
if it does not match; only proceed with clearing training_day, contest, num,
shifting task numbers, committing and reinitializing when the task is confirmed
to belong to that managing_contest.

In `@cms/server/admin/static/aws_table_utils.js`:
- Around line 24-35: The comparator in CMS.AWSTableUtils.getRowComparator reads
only "td" cells, so columns using <th> (row headers) are ignored; update the
selectors in that function (the variables cellA and cellB where you call
.children("td").eq(column_idx)) to select both cell types by using "td,th"
instead of "td" so the comparator reads data-value/text from either <td> or <th>
when determining valA and valB.

In `@cms/server/admin/static/training_program.js`:
- Line 10: The bootstrap reads CMS before its declaration; change the
initialization to reference the global explicitly by replacing the current var
CMS = CMS || {}; with a safe global access that uses window.CMS (e.g., var CMS =
window.CMS || {}), so the symbol CMS is not referenced before declaration and
the global namespace is preserved — update the CMS bootstrap initialization in
training_program.js accordingly.

In `@cms/server/admin/templates/fragments/user_detail_scripts.html`:
- Around line 92-125: UserDetail.do_show builds HTML by concatenating
contest['name'] and task['name'] directly, which allows HTML/script injection;
update UserDetail.do_show to escape these values before inserting into strings
(or preferably create DOM nodes and set textContent/.text() instead of string
concatenation). Locate UserDetail.do_show and replace occurrences of
contest['name'] and task['name'] (and any similar direct insertions like in the
global row if needed) with a safe-escaped variant (e.g., use an existing escape
helper or add one like escapeHtml and call it where contest['name'] and
task['name'] are used) or construct the row via document.createElement and
element.textContent to ensure names are safely encoded.

In `@cms/server/admin/templates/student_tasks.html`:
- Around line 128-153: The template uses a fixed threshold (>= 100) to mark
green/amber which is incorrect because training_scores and home_scores are raw
values; update the color logic to compare each score to the task's max score
(use st.task.max_score or whatever the task property is) or compute a normalized
percentage (score / st.task.max_score * 100) and then set green when score >=
st.task.max_score (or percent >= 100), amber when score > 0, else grey; change
the color expression in both the training_scores and home_scores blocks (the
spans that currently use '{{ '#059669' if score >= 100 else '#d97706' if score >
0 else '#cbd5e1' }}') to use the task max-based comparison or normalized
percentage.

In `@cms/server/admin/templates/training_program_combined_ranking_detail.html`:
- Around line 6-16: The "Edit Student" anchor should be rendered only when
student.participation exists to avoid a 500; wrap the <a href="{{
url("training_program", training_program.id, "student",
student.participation.user.id, "edit") }}">Edit Student</a> output in a
conditional like {% if student.participation %}...{% endif %} (or check
student.participation.user) so the template won't try to access
student.participation.user.id when participation is missing.
🟡 Minor comments (14)
cms/server/admin/static/aws_utils.js-832-845 (1)

832-845: ⚠️ Potential issue | 🟡 Minor

Ensure tags are cleared when no raw values are present.

If .announcement_raw_visible_to_tags is absent, the previous Tagify state can linger across edits. Clearing the input/tagify when the raw field is missing prevents accidental tag carry-over.

🧹 Suggested fix
-    const visibleToTagsInput = form.querySelector('input[name="visible_to_tags"]');
-    const rawVisibleToTags = notification.querySelector('.announcement_raw_visible_to_tags');
-    if (visibleToTagsInput && rawVisibleToTags) {
-        const rawValue = rawVisibleToTags.value;
-        const tagify = visibleToTagsInput._tagify;
-        if (tagify) {
-            tagify.removeAllTags();
-            const tags = rawValue.split(",").map(t => t.trim()).filter(Boolean);
-            if (tags.length) tagify.addTags(tags);
-        } else {
-            visibleToTagsInput.value = rawValue;
-        }
-    }
+    const visibleToTagsInput = form.querySelector('input[name="visible_to_tags"]');
+    const rawVisibleToTags = notification.querySelector('.announcement_raw_visible_to_tags');
+    if (visibleToTagsInput) {
+        const rawValue = rawVisibleToTags ? rawVisibleToTags.value : "";
+        const tagify = visibleToTagsInput._tagify;
+        if (tagify) {
+            tagify.removeAllTags();
+            const tags = rawValue.split(",").map(t => t.trim()).filter(Boolean);
+            if (tags.length) tagify.addTags(tags);
+        } else {
+            visibleToTagsInput.value = rawValue;
+        }
+    }
cms/server/admin/static/aws_form_utils.js-211-218 (1)

211-218: ⚠️ Potential issue | 🟡 Minor

Add guard for Tagify in initTagify to match initReadOnlyTagify.

initTagify instantiates Tagify at line 306 without checking if it's defined, unlike initReadOnlyTagify which guards on line 171. If the Tagify script fails to load, this will throw and break other page functionality. Add the same guard used elsewhere in the codebase:

♻️ Suggested guard
 CMS.AWSFormUtils.initTagify = function(config) {
+    if (typeof Tagify === 'undefined') {
+        return;
+    }
     var inputs = document.querySelectorAll(config.inputSelector);
     if (!inputs.length) return;
cms/server/admin/handlers/task.py-723-734 (1)

723-734: ⚠️ Potential issue | 🟡 Minor

Add from None to the raised exception to satisfy exception chaining best practices.

The static analysis tool (Ruff B904) correctly flags that exceptions raised within an except block should use raise ... from err or raise ... from None to distinguish intentional exception transformations from errors in exception handling.

Since the original exception is already logged with exc_info=True, using from None is appropriate here.

🐛 Proposed fix
             raise tornado.web.HTTPError(
                 500, f"Couldn't create default submission format for task {task.id}"
-            )
+            ) from None
cms/server/admin/templates/contest_tasks.html-122-122 (1)

122-122: ⚠️ Potential issue | 🟡 Minor

Potential error if visible_to_tags is None.

If t.visible_to_tags is None (rather than an empty list), the |join(', ') filter will fail. Consider adding a default:

🛡️ Suggested fix
-          <input type="text" class="task-visibility-tags" data-task-id="{{ t.id }}" value="{{ t.visible_to_tags|join(', ') }}"/>
+          <input type="text" class="task-visibility-tags" data-task-id="{{ t.id }}" value="{{ (t.visible_to_tags or [])|join(', ') }}"/>
cms/server/admin/templates/delays_and_extra_times.html-246-286 (1)

246-286: ⚠️ Potential issue | 🟡 Minor

Potential undefined variable ineligible_training_program.

Line 246 checks ineligible_students is defined, but Line 285 uses ineligible_training_program.id without verifying it's defined. If the server fails to provide this variable, the template will error.

Consider adding a guard or ensuring the server always provides both variables together:

🛡️ Suggested fix
-{% if ineligible_students is defined and ineligible_students|length > 0 %}
+{% if ineligible_students is defined and ineligible_students|length > 0 and ineligible_training_program is defined %}
cms/server/admin/handlers/contestannouncement.py-91-121 (1)

91-121: ⚠️ Potential issue | 🟡 Minor

Training-program announcements currently require non-empty text.
Contests allow empty text, so this is inconsistent and can block valid announcements. If empty text is acceptable, gate on subject only and keep the same notification as contests.

✅ Align with contest behavior
-        if subject and text:
+        if subject:
             if announcement_id is not None:
                 # Edit existing announcement
                 announcement = self.safe_get_item(Announcement, announcement_id)
                 if announcement.contest_id != managing_contest.id:
                     raise tornado.web.HTTPError(404)
                 announcement.subject = subject
                 announcement.text = text
                 announcement.visible_to_tags = visible_to_tags
             else:
                 # Add new announcement
                 announcement = Announcement(
                     timestamp=make_datetime(),
                     subject=subject,
                     text=text,
                     contest=managing_contest,
                     admin=self.current_user,
                     visible_to_tags=visible_to_tags,
                 )
                 self.sql_session.add(announcement)
             self.try_commit()
+        else:
+            self.service.add_notification(
+                make_datetime(), "Subject is mandatory.", "")
cms/server/admin/templates/student.html-210-214 (1)

210-214: ⚠️ Potential issue | 🟡 Minor

Add |format_datetime filter to timestamp display.

msg.timestamp is displayed without formatting in this template, while identical message displays in participation.html (line 196) and announcements.html (line 92) use the |format_datetime filter. Update to {{ msg.timestamp|format_datetime }} for consistency and readable output.

cms/db/task.py-126-129 (1)

126-129: ⚠️ Potential issue | 🟡 Minor

Use default=list instead of default=[] for ARRAY columns.

While SQLAlchemy copies mutable defaults safely, the best practice is to pass a callable (list) rather than a mutable object ([]). This pattern is already used elsewhere in the codebase (e.g., cms/db/student.py:71, cms/db/training_day.py:144), but several ARRAY columns inconsistently use default=[] instead. Standardize to default=list for consistency and to follow SQLAlchemy conventions. The same issue also appears in primary_statements (line 152 of this file) and visible_to_tags in cms/db/contest.py (line 443).

cms/server/admin/templates/training_program_students.html-185-196 (1)

185-196: ⚠️ Potential issue | 🟡 Minor

Confirm prompt has unescaped username - potential XSS vector.

Line 187 interpolates {{ u.username }} directly into a JavaScript string within an onclick attribute. If a username contains a single quote, it would break out of the string and could enable XSS.

🔧 Suggested fix using tojson for proper escaping
                                 <button type="submit" name="operation" value="remove_{{ u.id }}" class="btn-icon-only"
                                     {% if not admin.permission_all %}disabled{% endif %}
-                                    onclick="return confirm('Are you sure you want to remove {{ u.username }} from the training program?');"
+                                    onclick='return confirm("Are you sure you want to remove " + {{ u.username|tojson }} + " from the training program?");'
                                     title="Remove from program">
cms/server/admin/handlers/contestdelayrequest.py-210-229 (1)

210-229: ⚠️ Potential issue | 🟡 Minor

Case-insensitive tag matching for ineligible students.
Eligibility uses lowercase comparisons in check_training_day_eligibility; the ineligible-student list should match that behavior to avoid false “no/multiple main group” flags.

🛠️ Suggested fix
-            main_group_tags = {g.tag_name for g in training_day.groups}
+            main_group_tags = {g.tag_name.lower() for g in training_day.groups}
 ...
-                student_tags = set(student.student_tags or [])
+                student_tags = {t.lower() for t in (student.student_tags or [])}
                 matching_tags = student_tags & main_group_tags
cms/server/admin/handlers/student.py-118-164 (1)

118-164: ⚠️ Potential issue | 🟡 Minor

Add-student path should guard against existing membership.
The bulk-add flow checks for existing participation, but the single-add path doesn’t. That can trigger an IntegrityError or duplicate enrollment if the UI sends a stale user_id.

🛠️ Suggested fix
         user = self.safe_get_item(User, user_id)

+        existing_participation = (
+            self.sql_session.query(Participation)
+            .filter(Participation.contest == managing_contest)
+            .filter(Participation.user == user)
+            .first()
+        )
+        if existing_participation is not None:
+            self.service.add_notification(
+                make_datetime(),
+                "Invalid field(s)",
+                "User is already a student in this program",
+            )
+            self.redirect(fallback_page)
+            return
+
         # Set starting_time to now so the student can see everything immediately
cms/server/admin/handlers/utils.py-218-245 (1)

218-245: ⚠️ Potential issue | 🟡 Minor

Default score_precision when unset.
task.score_precision can be None in other parts of the codebase; returning None here risks downstream formatting/rounding issues.

🛠️ Suggested fix
-    score_precision = task.score_precision
+    score_precision = task.score_precision if task.score_precision is not None else 0
cms/server/admin/handlers/trainingday.py-757-769 (1)

757-769: ⚠️ Potential issue | 🟡 Minor

Normalize scoreboard-sharing tag keys to lowercase.
Student tags are normalized to lowercase elsewhere (e.g., parse_tags). Leaving mixed-case keys here can cause mismatches when resolving sharing rules.

📝 Suggested tweak
-                    if tag == "__everyone__":
-                        normalized_tag = tag
-                    else:
-                        normalized_tag = tag.strip()
+                    if tag == "__everyone__":
+                        normalized_tag = tag
+                    else:
+                        normalized_tag = tag.strip().lower()
cms/server/admin/handlers/contestranking.py-504-509 (1)

504-509: ⚠️ Potential issue | 🟡 Minor

Chain the parsing error to preserve debugging context.
This aligns with Ruff B904 (raise-without-from-inside-except), which flags exception raises inside except handlers that don't use exception chaining.

🔧 Suggested fix
-            except ValueError:
-                raise tornado.web.HTTPError(400, "Invalid main_group_user_ids parameter")
+            except ValueError as err:
+                raise tornado.web.HTTPError(
+                    400, "Invalid main_group_user_ids parameter"
+                ) from err
🧹 Nitpick comments (20)
cms/locale/cms.pot (1)

1267-1411: Consider de‑ambiguating single‑letter labels (“H:”, “T:”) for translators.
These are hard to localize without context; prefer full labels or add translator comments/context in the source so the POT carries that metadata.

cms/server/admin/static/aws_style.css (1)

1448-1478: Consolidate duplicate CSS rule definitions.

Several selectors are defined twice in this file, which creates maintenance burden and potential for inconsistencies:

Selector First definition Duplicate here
.menu_entry Lines 500-505 Lines 1448-1453
.menu_link Lines 514-522 Lines 1455-1458
.sidebar-section-title Lines 222-237 Lines 1461-1466
.sidebar-contest, .sidebar-item Lines 383-389 Lines 1469-1474
.sidebar-contest a, .sidebar-item a Lines 395-405 Lines 1477-1478

Consider consolidating each selector into a single rule block to avoid confusion and ensure consistent styling.

cms/server/admin/static/tagify/tagify.css (1)

1-1: Consider CSS fallbacks for older browsers if necessary for your target environment.
The :has() selector and lh unit are well-supported in modern browsers (Chrome 105+, Edge 105+, Firefox 121+, Safari 15.4+), so they're safe for most admin panel use cases. If your deployment requires support for older browsers, consider adding fallbacks like min-height: 1.5em before min-height: 1.5lh, or use @supports selector(:has(*)) for conditional rules.

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

11-18: Add eager loading for training_day and training_program relationships.

The template accesses contest.training_day and contest.training_day.training_program, which will trigger lazy loads if these relationships aren't eagerly fetched in the handler. Consider using:

self.contest = self.sql_session.query(Contest)\
    .options(
        joinedload(Contest.training_day)
        .joinedload(TrainingDay.training_program)
    )\
    .get(contest_id)

Or update safe_get_item() to accept options for the query, similar to the pattern already used in ContestHandler (contest.py:151).

cms/db/base.py (1)

276-285: Avoid catching TypeError from inside default callables.

The current try/except treats any TypeError as an arity mismatch, which can mask real errors thrown by the default callable itself. Consider inspecting the callable signature to decide whether to call with an argument, so genuine failures still surface.

♻️ Suggested refactor (signature-based arity detection)
+import inspect
 ...
-                if col.default is not None:
-                    if col.default.is_callable:
-                        try:
-                            # Try passing None (simulating a missing ExecutionContext)
-                            setattr(self, prp.key, col.default.arg(None))
-                        except TypeError:
-                            # Fallback for zero-argument callables (like simple lambdas)
-                            setattr(self, prp.key, col.default.arg())
-                    else:
-                        setattr(self, prp.key, col.default.arg)
+                if col.default is not None:
+                    if col.default.is_callable:
+                        default_callable = col.default.arg
+                        try:
+                            params = inspect.signature(default_callable).parameters
+                            expects_ctx = len(params) > 0
+                        except (TypeError, ValueError):
+                            # Fallback for non-introspectable callables
+                            expects_ctx = True
+                        value = default_callable(None) if expects_ctx else default_callable()
+                        setattr(self, prp.key, value)
+                    else:
+                        setattr(self, prp.key, col.default.arg)
cms/server/admin/handlers/user.py (1)

196-239: Consider eager loading to avoid N+1 contest lookups.

p.contest.training_program / p.contest.training_day inside the loop can trigger per-row lazy loads. A joined/select-in load on Participation.contest and its relationships keeps this linear.

♻️ Suggested eager loading
+from sqlalchemy.orm import joinedload
 ...
-        all_participations = self.sql_session.query(Participation)\
-            .filter(Participation.user == user)\
-            .all()
+        all_participations = (
+            self.sql_session.query(Participation)
+            .options(
+                joinedload(Participation.contest).joinedload(Contest.training_program),
+                joinedload(Participation.contest).joinedload(Contest.training_day),
+            )
+            .filter(Participation.user == user)
+            .all()
+        )
cms/server/admin/templates/fragments/user_detail_scripts.html (1)

101-127: Convert for...in loops to index-based loops for array iteration.

contest_list and contest["tasks"] are arrays (not objects), confirmed by their initialization with new Array() and array methods used throughout the codebase. The codebase already uses index-based loops elsewhere for these same arrays (DataStore.js:226-231, 621). For consistency and to avoid potential issues with inherited properties, convert both loops to index-based iteration.

♻️ Suggested refactor
-      for (var i in contests) {
-        var contest = contests[i];
+      for (var i = 0; i < contests.length; i++) {
+        var contest = contests[i];
@@
-        for (var j in tasks) {
-          var task = tasks[j];
+        for (var j = 0; j < tasks.length; j++) {
+          var task = tasks[j];
cms/server/admin/templates/macro/delay_request.html (1)

33-38: Consider extracting inline styles to a CSS class.

The warning block uses inline styles which work but could be consolidated into a reusable CSS class for consistency across the admin interface.

♻️ Optional: Extract to CSS class
/* In your CSS file */
.delay-request-warning {
  background-color: `#fff3cd`;
  border: 1px solid `#ffc107`;
  padding: 8px;
  margin: 8px 0;
  border-radius: 4px;
}
.delay-request-warning strong {
  color: `#856404`;
}

Then in the template:

<div class="delay-request-warning">
  <strong>Warning:</strong> ...
</div>
cms/server/admin/templates/fragments/user_detail_layout.html (1)

56-61: Consider making canvas dimensions responsive.

The canvas elements have hardcoded dimensions (width="920" height="200"). If this layout is used on screens with varying widths, consider using CSS to make the canvas responsive or setting dimensions via JavaScript based on container size.

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

74-91: Consider combining the two loops over training_days for efficiency.

The code iterates over training_program.training_days twice:

  1. Lines 75-80: To compute notifications (if not already cached)
  2. Lines 81-91: To build the list of days with unanswered questions

These could be combined into a single loop:

♻️ Suggested optimization
-            if not td_notifications:
-                for td in training_program.training_days:
-                    if td.contest is None:
-                        continue
-                    td_notifications[td.id] = get_training_day_notifications(
-                        self.sql_session, td
-                    )
-            for td in training_program.training_days:
-                if td.contest is None:
-                    continue
-                td_notif = td_notifications.get(td.id, {})
+            for td in training_program.training_days:
+                if td.contest is None:
+                    continue
+                if td.id not in td_notifications:
+                    td_notifications[td.id] = get_training_day_notifications(
+                        self.sql_session, td
+                    )
+                td_notif = td_notifications[td.id]
                 unanswered_count = td_notif.get("unanswered_questions", 0)
cms/server/admin/templates/fragments/training_day_groups.html (1)

78-91: Inconsistent form field naming for checkbox.

The other input fields use array notation (group_id[], start_time[], duration_hours[], etc.), but the checkbox uses alphabetical_{{ group.id }}. This may require different handling on the server side.

Consider using a consistent naming pattern. If the server expects array notation:

♻️ Option 1: Use hidden + checkbox pattern for array
       <td style="text-align: center;">
-        <input type="checkbox" name="alphabetical_{{ group.id }}" {{ "checked" if group.alphabetical_task_order else "" }}/>
+        <input type="hidden" name="alphabetical_order[]" value="false"/>
+        <input type="checkbox" name="alphabetical_order[]" value="true" {{ "checked" if group.alphabetical_task_order else "" }}/>
       </td>

Alternatively, the current approach is fine if the server handler explicitly iterates over group IDs and checks for alphabetical_{id} parameters.

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

245-263: Consider extracting validation into a helper method.

The inline ValueError raises with f-strings work correctly but could be cleaner with a dedicated validation helper. This is a minor suggestion aligned with the Ruff hints (TRY003, TRY301).

♻️ Optional: Extract validation helper
def _validate_training_day_times(training_day, new_start, new_stop):
    """Validate contest times don't conflict with training day group times."""
    if not training_day or not training_day.groups:
        return
    for group in training_day.groups:
        if group.start_time is not None and new_start is not None:
            if new_start > group.start_time:
                raise ValueError(
                    f"Training day start cannot be after main group "
                    f"'{group.tag_name}' start time"
                )
        if group.end_time is not None and new_stop is not None:
            if new_stop < group.end_time:
                raise ValueError(
                    f"Training day end cannot be before main group "
                    f"'{group.tag_name}' end time"
                )

391-396: Broad exception catch is flagged by static analysis.

Catching bare Exception (BLE001) can mask unexpected errors. Consider catching more specific exceptions if the expected failure modes are known (e.g., ValueError, AssertionError, KeyError).

♻️ Optional: Narrow exception types
-        except Exception as error:
+        except (ValueError, AssertionError) as error:
cms/server/admin/templates/student.html (1)

170-170: Consider using not for list emptiness checks.

Using != [] works but {% if participation.questions %} (truthy check) is more idiomatic in Jinja2.

♻️ More idiomatic empty list check
-  {% if participation.questions != [] %}
+  {% if participation.questions %}

Same applies to line 207:

-    {% if participation.messages != [] %}
+    {% if participation.messages %}
cms/server/admin/templates/training_program_combined_ranking.html (2)

218-221: Complex URL construction with filter parameters.

The export URL building inline with multiple conditionals is hard to read and maintain. Consider using Jinja2's url_for with query params or building the params list programmatically (similar to lines 281-286).

♻️ Suggested: Use consistent URL param building
{% set export_params = [] %}
{% if start_date %}{% set _ = export_params.append('start_date=' ~ start_date.strftime('%Y-%m-%d')) %}{% endif %}
{% if end_date %}{% set _ = export_params.append('end_date=' ~ end_date.strftime('%Y-%m-%d')) %}{% endif %}
{% if training_day_types %}{% set _ = export_params.append('training_day_types=' ~ (training_day_types | join(',') | urlencode)) %}{% endif %}
{% if student_tags %}{% set _ = export_params.append('student_tags=' ~ (student_tags | join(',') | urlencode)) %}{% endif %}
{% if student_tags_mode %}{% set _ = export_params.append('student_tags_mode=' ~ student_tags_mode) %}{% endif %}

<a href="{{ url('training_program', training_program.id, 'combined_ranking', 'export') }}{% if export_params %}?{{ export_params | join('&') }}{% endif %}" ...>

17-47: Potential performance concern with nested template loops generating JS.

For training programs with many students and training days, the nested Jinja2 loops generating JavaScript objects (lines 18-23, 30-47, 70-82) could produce large output. Consider whether this data should be served via a separate JSON endpoint for very large datasets.

cms/db/training_day.py (1)

174-178: Consider adding passive_deletes=True for consistency.

The groups relationship has cascade="all, delete-orphan" but lacks passive_deletes=True. Since TrainingDayGroup has ondelete="CASCADE" on its FK (per the relevant code snippets), adding passive_deletes=True would allow the database to handle deletions directly without SQLAlchemy issuing separate DELETE statements, improving performance on bulk deletes.

This is consistent with how the submissions relationship (line 183) is configured.

🔧 Suggested change
     groups: list["TrainingDayGroup"] = relationship(
         "TrainingDayGroup",
         back_populates="training_day",
         cascade="all, delete-orphan",
+        passive_deletes=True,
     )
cms/server/admin/handlers/base.py (2)

278-295: Avoid user-facing notifications on try_commit failure.

The current failure notification conflicts with the established pattern for admin handlers. Consider relying on centralized error handling/logging instead.

🔧 Suggested adjustment
-        except IntegrityError as error:
-            self.service.add_notification(
-                make_datetime(), "Operation failed.", "%s" % error
-            )
-            return False
+        except IntegrityError:
+            return False

Based on learnings: 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.


350-413: Avoid swallowing unexpected exceptions silently.

except Exception: pass in request preparation hides redirect failures and makes debugging harder. Logging the exception keeps behavior resilient while preserving observability.

🔧 Suggested adjustment
-            except Exception:
-                pass
+            except Exception:
+                logger.exception("Failed to apply training program redirect")
cms/server/admin/handlers/trainingday.py (1)

694-702: Align try_commit failure handling with admin handler pattern.
Both handlers currently return explicit “Failed to save” JSON responses when try_commit() fails. The admin handlers typically rely on centralized error handling/logging instead of explicit failure notifications. Consider removing the custom error payload and raising/propagating a 500 instead.

🔧 Example adjustment (apply similarly in both handlers)
-            if self.try_commit():
-                self.write({
-                    "success": True,
-                    "types": training_day.training_day_types
-                })
-            else:
-                self.set_status(500)
-                self.write({"error": "Failed to save"})
+            if not self.try_commit():
+                raise tornado.web.HTTPError(500)
+            self.write({
+                "success": True,
+                "types": training_day.training_day_types
+            })
Based on learnings: 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.

Also applies to: 799-807

ronryv and others added 2 commits February 3, 2026 13:45
- Create SVG sprite fragment (svg_icons.html) with 30+ icon symbols using Feather icon style
- Update base.html to include sprite and use <use> references for all sidebar menu icons
- Add icons to contest menu items (General, Ranking, Submissions, etc.)
- Add icons to training program menu items (Home Task Scores, Submissions, Students, etc.)
- Update Training Days folder icon to use SVG sprite
- Rename 'Combined Ranking' to 'Ranking' in training program sidebar
- Update training_programs.html to use sprite references for all inline SVGs
- Update training_program_training_days.html to use sprite references
- Add CSS styles for svg.icon and .folder-icon-svg classes

Co-Authored-By: Ron Ryvchin <[email protected]>
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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cms/server/admin/handlers/contestdelayrequest.py (1)

362-399: ⚠️ Potential issue | 🟡 Minor

Exclude ineligible training-day participations from the CSV export.
The UI filters these out, but the export currently includes them, which can skew attendance reporting.

🔧 Suggested fix
         for participation in participations:
+            if self.contest.training_day is not None:
+                is_eligible, _, _ = check_training_day_eligibility(
+                    self.sql_session, participation, self.contest.training_day
+                )
+                if not is_eligible:
+                    continue
             starting_time = participation.starting_time.strftime('%Y-%m-%d %H:%M:%S') if participation.starting_time else '-'
🤖 Fix all issues with AI agents
In `@cms/grading/scorecache.py`:
- Around line 762-797: The current code in the training-day branch (uses
get_managing_participation, training_day, managing_participation and queries
Submission) raises a ValueError when managing_participation is None; change this
to a safe fallback: log the missing managing participation (use the module
logger, e.g., logger.warning or similar) with context including
participation.user_id and training_day.id, then return an empty list instead of
raising so cache rebuilds/rankings don't crash; keep the rest of the query logic
for the non-training case unchanged.

In `@cms/server/admin/handlers/base.py`:
- Around line 350-413: The bare "except Exception: pass" in prepare() swallows
DB/routing errors; replace it with an except Exception as e block that logs the
error (including stack trace) before proceeding—use self.logger.exception(...)
if available (fallback to logging.exception(...)) and either re-raise or at
least retain the original behavior after logging; update the except around the
self.sql_session.query(Contest)/contest.training_program/url_mappings/tp_url
redirect logic so errors are recorded instead of silently ignored.
- Around line 609-656: render_params_for_students_page currently sets
self.r_params["all_tasks"] using managing_contest.get_tasks(), which will
include training-day tasks; update this to exclude training-day tasks by
querying tasks where Task.contest == managing_contest and
Task.training_day_id.is_(None) (mirroring the pattern used in
ContestTasksHandler) so bulk-assign only shows non-training-day tasks; locate
the assignment to self.r_params["all_tasks"] in render_params_for_students_page
and replace it with the filtered query using the Task model and
self.sql_session.

In `@cms/server/admin/handlers/contestannouncement.py`:
- Around line 76-121: The current post method in this handler requires both
subject and text and thus silently no-ops when text is empty; change validation
to require subject only (mirror contest handlers) and on edits only overwrite
Announcement.text when a non-empty text argument is provided while still
updating subject and visible_to_tags; for new Announcement allow text to be
empty. Update the post method (the function named post and code paths that touch
Announcement creation/editing, announcement.text assignment, and the
announcement_id branch) so the outer check is if subject: and inside the edit
branch set announcement.text = text only when text is non-empty (leave existing
text otherwise).

In `@cms/server/admin/handlers/contestranking.py`:
- Around line 503-509: The except block in the main_group_user_ids parsing code
should chain the original ValueError when raising the HTTPError to preserve
exception context; update the except ValueError handler in the
main_group_user_ids_param processing (the block creating main_group_user_ids
set) to capture the ValueError as a variable (e.g., "err") and re-raise using
"raise tornado.web.HTTPError(400, 'Invalid main_group_user_ids parameter') from
err".

In `@cms/server/admin/handlers/student.py`:
- Around line 150-160: When adding td_participation inside the loop over
training_program.training_days, avoid creating duplicates by first checking
whether a Participation already exists for that training_day.contest and the
same user; if a matching Participation is found, skip creating/adding a new
td_participation. Implement the same presence check (querying Participation for
contest=training_day.contest and user/user_id) before calling
self.sql_session.add(td_participation), and apply this guard to the other
bulk-add loop that also creates td_participation.

In `@cms/server/admin/handlers/task.py`:
- Around line 723-734: The except block that catches any Exception should
preserve the original exception when raising the HTTPError: change the handler
to capture the exception (e.g., except Exception as e:) and raise
tornado.web.HTTPError(500, f"Couldn't create default submission format for task
{task.id}") from e so the original traceback is chained; update the block around
logger.error and the raise of tornado.web.HTTPError to use the captured
exception variable in the from clause.

In `@cms/server/admin/handlers/trainingprogramtask.py`:
- Around line 277-279: The code sets Task.num with task.num =
len(managing_contest.tasks), which can collide when tasks have gaps; instead
compute the next index by finding the current maximum num among
managing_contest.tasks and using max_num + 1 (or 0 if none) before assigning to
task.num and then set task.contest = managing_contest; update the logic around
Task.num assignment in trainingprogramtask (look for task.num and
managing_contest.tasks) to use this max-based approach.

In `@cms/server/admin/static/js/admin_table_utils.js`:
- Around line 72-86: The dragover handler currently inserts the placeholder
relative to any TR; update the guard in the tbody.addEventListener('dragover')
block to skip rows that are not draggable by checking the same attribute used in
dragstart (e.g., verify targetRow has the data draggable flag or attribute).
Specifically, after computing targetRow (from e.target.closest(rowSelector)),
also return early if the targetRow does not have the draggable marker (for
example targetRow.getAttribute('data-draggable') !== 'true' or
!targetRow.hasAttribute('draggable')), so placeholder insertion only happens for
actual draggable rows (use the existing variables targetRow, draggedRow,
placeholder, rowSelector to locate code).

In `@cms/server/admin/templates/training_program_student_remove.html`:
- Line 21: The anchor used for the destructive "Yes, remove" action isn't
reliably keyboard-focusable; replace the <a> with a semantic <button
type="button"> (keep the existing class "button-link" and the onclick call to
CMS.AWSUtils.ajax_delete('{{ url("training_program", training_program.id,
"student", user.id, "remove") }}')) so the control is keyboard-accessible and
activatable via Enter/Space, or alternatively add a valid href and call
event.preventDefault() inside the same onclick handler—ensure the element
remains focusable and preserves the existing onclick invocation.
🧹 Nitpick comments (11)
cms/server/admin/templates/macro/delay_request.html (1)

33-38: Consider extracting inline styles to a CSS class for consistency.

The warning block uses inline styles which works but could benefit from a CSS class for easier maintenance and consistency with other styled components in the admin UI. This is a minor suggestion that can be deferred.

cms/server/admin/static/aws_tp_styles.css (1)

2036-2041: Consider using only standard flexbox for multiline text truncation.

The display: box property is non-standard and deprecated. While -webkit-line-clamp is widely supported now, it's best to use only the prefixed properties that are part of the working spec.

♻️ Suggested fix
     overflow: hidden;
     text-overflow: ellipsis;
     display: -webkit-box;
-    display: box;
     -webkit-line-clamp: 2;
     line-clamp: 2;
     -webkit-box-orient: vertical;
cms/server/admin/static/aws_form_utils.js (2)

272-303: Consider potential race condition in user-initiated removal detection.

The 200ms threshold for userRemovalTriggeredAt timing detection is a reasonable heuristic, but could occasionally misclassify events on slow machines or under heavy CPU load. The current implementation errs on the side of not requiring confirmation for edge cases, which is acceptable UX.


381-387: Timing-based arming approach is reasonable but fragile.

The 100ms setTimeout to arm confirmations avoids dialogs during initial page load. However, this could fail on very slow page loads where Tagify takes longer than 100ms to initialize pre-existing tags.

Consider using Tagify's internal state or event to determine when initial load is complete, though the current approach is pragmatic for typical scenarios.

cms/server/admin/handlers/base.py (3)

121-157: Preserve original exception context in parsers.
For the parsers that wrap exceptions (parse_int, parse_timedelta_*, parse_datetime), chaining with from err keeps diagnostics intact.

🧩 Exception chaining
-    except (ValueError, TypeError):
-        raise ValueError("Can't cast %s to int." % value)
+    except (ValueError, TypeError) as err:
+        raise ValueError("Can't cast %s to int." % value) from err
-    except (ValueError, TypeError):
-        raise ValueError("Can't cast %s to timedelta." % value)
+    except (ValueError, TypeError) as err:
+        raise ValueError("Can't cast %s to timedelta." % value) from err
-    except (ValueError, TypeError):
-        raise ValueError("Can't cast %s to timedelta." % value)
+    except (ValueError, TypeError) as err:
+        raise ValueError("Can't cast %s to timedelta." % value) from err
-    except (ValueError, TypeError):
-        raise ValueError("Can't cast %s to datetime." % value)
+    except (ValueError, TypeError) as err:
+        raise ValueError("Can't cast %s to datetime." % value) from err

278-295: Avoid emitting notifications inside try_commit.
Centralized error handling keeps a consistent UX and avoids duplicate messages; consider letting callers decide when to notify.

🔧 Possible adjustment
-        except IntegrityError as error:
-            self.service.add_notification(
-                make_datetime(), "Operation failed.", "%s" % error
-            )
-            return False
-        else:
-            self.service.add_notification(make_datetime(), "Operation successful.", "")
-            return True
+        except IntegrityError:
+            return False
+        else:
+            return True

Based on learnings: 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.


757-792: Exception chaining would improve error diagnostics.
These parsing helpers drop the original exception context; chaining keeps root causes visible in logs.

🧩 Exception chaining
-            except ValueError as e:
-                raise ValueError("Can't cast %s to float: %s" % (value, e))
+            except ValueError as err:
+                raise ValueError("Can't cast %s to float: %s" % (value, err)) from err
-            except ValueError as e:
-                raise ValueError("Can't cast %s to int: %s" % (value, e))
+            except ValueError as err:
+                raise ValueError("Can't cast %s to int: %s" % (value, err)) from err
cms/server/admin/handlers/contestquestion.py (1)

70-93: Consider potential redundant computation of training day notifications.

Lines 74-80 recompute notifications for each training day if td_notifications is empty. However, render_params_for_training_program (called by setup_contest_or_training_program) likely already populates training_day_notifications in r_params. The condition if not td_notifications would only trigger if the helper didn't populate it, which could indicate a code path inconsistency.

Consider verifying whether this fallback computation is actually needed, or if it can be removed to simplify the code.

#!/bin/bash
# Check if render_params_for_training_program populates training_day_notifications
rg -n "training_day_notifications" --type py cms/server/admin/handlers/base.py -A 3 -B 3
cms/server/admin/templates/fragments/training_day_groups.html (1)

90-92: Checkbox naming pattern uses embedded ID instead of array notation.

The checkbox uses name="alphabetical_{{ group.id }}" which embeds the group ID in the input name. This differs from the array notation (group_alphabetical[]) used in add_training_day.html. The server handler must parse these differently.

This is a design choice rather than a bug, but ensure consistency is intentional and the handler correctly processes this format.

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

243-249: Minor inconsistency in toggle selector for single-table path.

The single-table JS toggle on lines 245-247 uses $("#ranking-table") (ID selector), while the multi-group path uses $(".td-ranking-table") (class selector). This works because the single table has both id="ranking-table" and class="td-ranking-table", but consider using the class selector consistently for maintainability:

🔧 Suggested improvement
     $(".show-indicators-toggle").on("change", function() {
       if (this.checked) {
-        $("#ranking-table").removeClass("hide-indicators");
+        $(".td-ranking-table").removeClass("hide-indicators");
       } else {
-        $("#ranking-table").addClass("hide-indicators");
+        $(".td-ranking-table").addClass("hide-indicators");
       }
     });
cms/server/admin/handlers/contestranking.py (1)

307-314: Consider precomputing task access to avoid N×M lookups.

In training-day contests, can_access_task typically hits the DB per call; the nested loop can explode to participations×tasks queries. Consider prefetching students/tags once (you already batch-fetch later) and computing access in-memory to keep this O(1) queries.

- Update menu_entry to have padding and hover background effect on entire row
- Icon and text now highlight together on hover (icon turns accent color)
- Fix Training Days folder header alignment to match menu items
- Consistent 12px spacing between icon and text
- Remove conflicting CSS rules for cleaner styling

Co-Authored-By: Ron Ryvchin <[email protected]>
@ronryv ronryv force-pushed the training_program branch 3 times, most recently from 2f0ec81 to 9032d98 Compare February 3, 2026 14:13
@ronryv ronryv force-pushed the training_program branch 2 times, most recently from 8e7aa13 to ff6e5a6 Compare February 3, 2026 16:21
For training days, participations are in the managing contest, not the
training day's contest. Filter by managing_contest_id when the contest
is a training day.

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

sonarqubecloud bot commented Feb 3, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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