Skip to content
Merged
1,252 changes: 52 additions & 1,200 deletions cms/server/admin/handlers/archive.py

Large diffs are not rendered by default.

123 changes: 121 additions & 2 deletions cms/server/admin/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
DelayRequest,
Participation,
Question,
Student,
Submission,
SubmissionResult,
Task,
Expand All @@ -69,9 +70,10 @@
from cms.grading.scoretypes import get_score_type_class
from cms.grading.tasktypes import get_task_type_class
from cms.server import CommonRequestHandler, FileHandlerMixin
from cms.server.util import (
exclude_internal_contests,
from cms.server.util import exclude_internal_contests, calculate_task_archive_progress
from cms.server.admin.handlers.utils import (
count_unanswered_questions,
get_all_student_tags,
get_all_training_day_notifications,
)
from cmscommon.crypto import hash_password, parse_authentication
Expand Down Expand Up @@ -553,6 +555,55 @@ def render_params_for_training_program(

return self.r_params

def render_params_for_students_page(
self, training_program: "TrainingProgram"
) -> dict:
"""Prepare render params for the training program students page.

This is a convenience method that sets up all the params needed
for the students page, including unassigned users, student progress,
and task/tag lists for the bulk assign modal.

Must be called after render_params_for_training_program().

Args:
training_program: The training program being viewed.

Returns:
The updated r_params dict.
"""
managing_contest = training_program.managing_contest

assigned_user_ids_q = self.sql_session.query(Participation.user_id).filter(
Participation.contest == managing_contest
)

self.r_params["unassigned_users"] = (
self.sql_session.query(User)
.filter(~User.id.in_(assigned_user_ids_q))
.filter(~User.username.like(r"\_\_%", escape="\\"))
.all()
)

# Calculate task archive progress for each student using shared utility
student_progress = {}
for student in training_program.students:
student_progress[student.id] = calculate_task_archive_progress(
student, student.participation, managing_contest, self.sql_session
)
# Commit to release any advisory locks taken by get_cached_score_entry
self.sql_session.commit()

self.r_params["student_progress"] = student_progress

# For bulk assign task modal
self.r_params["all_tasks"] = managing_contest.get_tasks()
self.r_params["all_student_tags"] = get_all_student_tags(
self.sql_session, training_program
)

return self.r_params

def write_error(self, status_code, **kwargs):
if "exc_info" in kwargs and kwargs["exc_info"][0] != tornado.web.HTTPError:
exc_info = kwargs["exc_info"]
Expand Down Expand Up @@ -904,6 +955,74 @@ def get_login_url(self) -> str:
return self.url("login")


class StudentBaseHandler(BaseHandler):
"""Base handler for student-related pages in a training program.

This handler provides common functionality for looking up a student's
context (training_program, managing_contest, participation, student)
and raises 404 if the student is not found.

Subclasses should call setup_student_context() at the start of their
get/post methods to populate self.training_program, self.managing_contest,
self.participation, and self.student.
"""

training_program: TrainingProgram
managing_contest: Contest
participation: Participation
student: Student

def setup_student_context(
self, training_program_id: str, user_id: str
) -> None:
"""Look up and set the student context for this request.

This method looks up the training program, managing contest,
participation, and student for the given IDs. It raises a 404
error if the participation or student is not found.

Args:
training_program_id: The training program ID from the URL.
user_id: The user ID from the URL.

Raises:
tornado.web.HTTPError(404): If participation or student not found.
"""
try:
user_id_int = int(user_id)
except ValueError:
raise tornado.web.HTTPError(404)

self.training_program = self.safe_get_item(
TrainingProgram, training_program_id
)
self.managing_contest = self.training_program.managing_contest
self.contest = self.managing_contest

participation: Participation | None = (
self.sql_session.query(Participation)
.filter(Participation.contest_id == self.managing_contest.id)
.filter(Participation.user_id == user_id_int)
.first()
)

if participation is None:
raise tornado.web.HTTPError(404)

student: Student | None = (
self.sql_session.query(Student)
.filter(Student.participation == participation)
.filter(Student.training_program == self.training_program)
.first()
)

if student is None:
raise tornado.web.HTTPError(404)

self.participation = participation
self.student = student


class FileHandler(BaseHandler, FileHandlerMixin):
pass

Expand Down
4 changes: 2 additions & 2 deletions cms/server/admin/handlers/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from cmscommon.datetime import make_datetime
from sqlalchemy.orm import joinedload
from sqlalchemy import func
from cms.server.util import get_all_student_tags
from cms.server.admin.handlers.utils import get_all_student_tags

from .base import BaseHandler, SimpleContestHandler, SimpleHandler, \
require_permission
Expand Down Expand Up @@ -158,7 +158,7 @@ def get(self, contest_id: str):
training_day = self.contest.training_day
if training_day is not None:
training_program = training_day.training_program
all_student_tags = get_all_student_tags(training_program)
all_student_tags = get_all_student_tags(self.sql_session, training_program)
self.r_params["all_student_tags"] = all_student_tags

self.render("contest.html", **self.r_params)
Expand Down
6 changes: 4 additions & 2 deletions cms/server/admin/handlers/contestannouncement.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
import tornado.web

from cms.db import Contest, Announcement
from cms.server.util import get_all_student_tags, parse_tags
from cms.server.admin.handlers.utils import get_all_student_tags, parse_tags
from cmscommon.datetime import make_datetime
from .base import BaseHandler, require_permission

Expand All @@ -56,7 +56,9 @@ def get(self, contest_id: str):
training_day = self.contest.training_day
if training_day is not None:
training_program = training_day.training_program
self.r_params["all_student_tags"] = get_all_student_tags(training_program)
self.r_params["all_student_tags"] = get_all_student_tags(
self.sql_session, training_program
)
self.r_params["is_training_day"] = True
else:
self.r_params["all_student_tags"] = []
Expand Down
7 changes: 5 additions & 2 deletions cms/server/admin/handlers/contestdelayrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
from cms.db import Contest, DelayRequest, Participation
from cms.server.contest.phase_management import compute_actual_phase
from cmscommon.datetime import make_datetime
from cms.server.util import check_training_day_eligibility, get_all_student_tags
from cms.server.util import check_training_day_eligibility
from cms.server.admin.handlers.utils import get_all_student_tags
from .base import BaseHandler, require_permission


Expand Down Expand Up @@ -211,7 +212,9 @@ def get(self, contest_id):
self.r_params["ineligible_training_program"] = training_program

# Collect all unique student tags for autocomplete (using shared utility)
self.r_params["all_student_tags"] = get_all_student_tags(training_program)
self.r_params["all_student_tags"] = get_all_student_tags(
self.sql_session, training_program
)

# Find students with 0 or >1 main group tags
ineligible = []
Expand Down
7 changes: 5 additions & 2 deletions cms/server/admin/handlers/contestranking.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
Submission, SubmissionResult, Task

from cms.grading.scorecache import get_cached_score_entry, ensure_valid_history
from cms.server.util import can_access_task, get_all_student_tags, get_student_for_user_in_program
from cms.server.util import can_access_task, get_student_for_user_in_program
from cms.server.admin.handlers.utils import get_all_student_tags
from .base import BaseHandler, require_permission

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -398,7 +399,9 @@ def get_group_score(p, tasks=accessible_tasks):
)

# Get all student tags for display
self.r_params["all_student_tags"] = get_all_student_tags(training_program)
self.r_params["all_student_tags"] = get_all_student_tags(
self.sql_session, training_program
)

self.r_params["main_groups_data"] = main_groups_data
self.r_params["student_tags_by_participation"] = student_tags_by_participation
Expand Down
10 changes: 7 additions & 3 deletions cms/server/admin/handlers/contesttask.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"""

from cms.db import Contest, Task
from cms.server.util import get_all_student_tags, deduplicate_preserving_order
from cms.server.admin.handlers.utils import get_all_student_tags, deduplicate_preserving_order
from cmscommon.datetime import make_datetime

from .base import BaseHandler, require_permission
Expand Down Expand Up @@ -76,7 +76,9 @@ def get(self, contest_id):
self.r_params["program_task_ids"] = [t.id for t in program_tasks]

# Get all student tags for autocomplete (for task visibility tags)
self.r_params["all_student_tags"] = get_all_student_tags(training_program)
self.r_params["all_student_tags"] = get_all_student_tags(
self.sql_session, training_program
)
else:
# For regular contests, show all unassigned tasks
self.r_params["unassigned_tasks"] = \
Expand Down Expand Up @@ -354,7 +356,9 @@ def post(self, contest_id, task_id):

# Get allowed tags from training program
training_program = training_day.training_program
allowed_tags = set(get_all_student_tags(training_program))
allowed_tags = set(get_all_student_tags(
self.sql_session, training_program
))

# Validate and filter tags against allowed set
invalid_tags = [tag for tag in incoming_tags if tag not in allowed_tags]
Expand Down
2 changes: 1 addition & 1 deletion cms/server/admin/handlers/contestuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

from cms.db import Contest, Message, Participation, Submission, User, Team, TrainingDay
from cms.db.training_day import get_managing_participation
from cms.server.util import parse_usernames_from_file
from cms.server.admin.handlers.utils import parse_usernames_from_file
from cmscommon.crypto import validate_password_strength
from cmscommon.datetime import make_datetime
from .base import BaseHandler, require_permission
Expand Down
Loading
Loading