Skip to content
Merged
16 changes: 9 additions & 7 deletions cms/server/admin/handlers/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
can_access_task,
check_training_day_eligibility,
parse_tags,
get_student_for_user_in_program,
build_user_to_student_map,
)
from cmscommon.datetime import make_datetime

Expand Down Expand Up @@ -537,13 +537,14 @@ def _archive_attendance_data(
"""Extract and store attendance data for all students."""
training_program = training_day.training_program

# Build user_id -> Student map for O(1) lookups instead of repeated queries
user_to_student = build_user_to_student_map(training_program)

for participation in contest.participations:
# Find the student for this user in the training program
# Note: Student.participation_id points to the managing contest participation,
# not the training day participation, so we need to look up by user_id
student = get_student_for_user_in_program(
self.sql_session, training_program, participation.user_id
)
student = user_to_student.get(participation.user_id)

if student is None:
continue
Expand Down Expand Up @@ -665,13 +666,14 @@ def _archive_ranking_data(
}
training_day.archived_tasks_data = archived_tasks_data

# Build user_id -> Student map for O(1) lookups instead of repeated queries
user_to_student = build_user_to_student_map(training_program)

for participation in contest.participations:
# Find the student for this user in the training program
# Note: Student.participation_id points to the managing contest participation,
# not the training day participation, so we need to look up by user_id
student = get_student_for_user_in_program(
self.sql_session, training_program, participation.user_id
)
student = user_to_student.get(participation.user_id)

if student is None:
continue
Expand Down
49 changes: 49 additions & 0 deletions cms/server/admin/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
exclude_internal_contests,
count_unanswered_questions,
get_all_training_day_notifications,
get_all_student_tags,
calculate_task_archive_progress,
)
from cmscommon.crypto import hash_password, parse_authentication
from cmscommon.datetime import make_datetime, get_timezone, local_to_utc, format_datetime_for_input, get_timezone_name
Expand Down Expand Up @@ -553,6 +555,53 @@ 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(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
168 changes: 15 additions & 153 deletions cms/server/admin/handlers/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
)
from cms.server.util import (
get_all_student_tags,
calculate_task_archive_progress,
get_student_archive_scores,
get_student_context,
get_submission_counts_by_task,
parse_tags,
parse_usernames_from_file,
Expand All @@ -55,37 +55,11 @@ class TrainingProgramStudentsHandler(BaseHandler):
@require_permission(BaseHandler.AUTHENTICATED)
def get(self, training_program_id: str):
training_program = self.safe_get_item(TrainingProgram, training_program_id)
managing_contest = training_program.managing_contest

self.render_params_for_training_program(training_program)

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
self.render_params_for_students_page(training_program)
self.r_params["bulk_add_results"] = None

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

self.render("training_program_students.html", **self.r_params)

@require_permission(BaseHandler.PERMISSION_ALL)
Expand Down Expand Up @@ -261,31 +235,9 @@ def post(self, training_program_id: str):
return

self.render_params_for_training_program(training_program)

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()
)

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
self.render_params_for_students_page(training_program)
self.r_params["bulk_add_results"] = results
self.r_params["students_added"] = students_added
self.r_params["all_tasks"] = managing_contest.get_tasks()
self.r_params["all_student_tags"] = get_all_student_tags(training_program)
self.render("training_program_students.html", **self.r_params)

except Exception as error:
Expand Down Expand Up @@ -408,28 +360,10 @@ class StudentHandler(BaseHandler):
@require_permission(BaseHandler.AUTHENTICATED)
def get(self, training_program_id: str, user_id: str):
training_program = self.safe_get_item(TrainingProgram, training_program_id)
managing_contest = training_program.managing_contest
self.contest = managing_contest

participation: Participation | None = (
self.sql_session.query(Participation)
.filter(Participation.contest_id == managing_contest.id)
.filter(Participation.user_id == user_id)
.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 == training_program)
.first()
training_program, managing_contest, participation, student = get_student_context(
self.sql_session, training_program, user_id
)

if student is None:
raise tornado.web.HTTPError(404)
self.contest = managing_contest

submission_query = self.sql_session.query(Submission).filter(
Submission.participation == participation
Expand Down Expand Up @@ -600,28 +534,10 @@ class StudentTasksHandler(BaseHandler):
@require_permission(BaseHandler.AUTHENTICATED)
def get(self, training_program_id: str, user_id: str):
training_program = self.safe_get_item(TrainingProgram, training_program_id)
managing_contest = training_program.managing_contest

participation: Participation | None = (
self.sql_session.query(Participation)
.filter(Participation.contest_id == managing_contest.id)
.filter(Participation.user_id == user_id)
.first()
training_program, managing_contest, participation, student = get_student_context(
self.sql_session, training_program, user_id
)

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 == training_program)
.first()
)

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

# Get all tasks in the training program for the "add task" dropdown
all_tasks = managing_contest.get_tasks()
assigned_task_ids = {st.task_id for st in student.student_tasks}
Expand Down Expand Up @@ -688,33 +604,15 @@ class StudentTaskSubmissionsHandler(BaseHandler):
@require_permission(BaseHandler.AUTHENTICATED)
def get(self, training_program_id: str, user_id: str, task_id: str):
training_program = self.safe_get_item(TrainingProgram, training_program_id)
managing_contest = training_program.managing_contest
task = self.safe_get_item(Task, task_id)
training_program, managing_contest, participation, student = get_student_context(
self.sql_session, training_program, user_id
)

# Validate task belongs to the training program
if task.contest_id != managing_contest.id:
raise tornado.web.HTTPError(404)

participation: Participation | None = (
self.sql_session.query(Participation)
.filter(Participation.contest_id == managing_contest.id)
.filter(Participation.user_id == user_id)
.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 == training_program)
.first()
)

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

# Verify student is assigned this specific task
student_task = (
self.sql_session.query(StudentTask)
Expand Down Expand Up @@ -755,28 +653,10 @@ def post(self, training_program_id: str, user_id: str):
)

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

participation: Participation | None = (
self.sql_session.query(Participation)
.filter(Participation.contest_id == managing_contest.id)
.filter(Participation.user_id == user_id)
.first()
training_program, managing_contest, participation, student = get_student_context(
self.sql_session, training_program, user_id
)

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 == training_program)
.first()
)

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

try:
task_id = self.get_argument("task_id")
if task_id in ("", "null"):
Expand Down Expand Up @@ -834,28 +714,10 @@ def post(self, training_program_id: str, user_id: str, task_id: str):
)

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

participation: Participation | None = (
self.sql_session.query(Participation)
.filter(Participation.contest_id == managing_contest.id)
.filter(Participation.user_id == user_id)
.first()
training_program, managing_contest, participation, student = get_student_context(
self.sql_session, training_program, user_id
)

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 == training_program)
.first()
)

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

student_task: StudentTask | None = (
self.sql_session.query(StudentTask)
.filter(StudentTask.student_id == student.id)
Expand Down
Loading
Loading