Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cms/server/admin/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@
TrainingProgramAttendanceHandler, \
TrainingProgramCombinedRankingHandler, \
TrainingProgramCombinedRankingHistoryHandler, \
TrainingProgramCombinedRankingDetailHandler
TrainingProgramCombinedRankingDetailHandler, \
TrainingProgramOverviewRedirectHandler, \
TrainingProgramResourcesListRedirectHandler


HANDLERS = [
Expand Down Expand Up @@ -366,6 +368,8 @@
(r"/training_program/([0-9]+)/combined_ranking", TrainingProgramCombinedRankingHandler),
(r"/training_program/([0-9]+)/combined_ranking/history", TrainingProgramCombinedRankingHistoryHandler),
(r"/training_program/([0-9]+)/student/([0-9]+)/combined_ranking_detail", TrainingProgramCombinedRankingDetailHandler),
(r"/training_program/([0-9]+)/overview", TrainingProgramOverviewRedirectHandler),
(r"/training_program/([0-9]+)/resourceslist", TrainingProgramResourcesListRedirectHandler),

# Training day groups (main groups configuration on contest page)
(r"/contest/([0-9]+)/training_day_group/add", AddTrainingDayGroupHandler),
Expand Down
9 changes: 6 additions & 3 deletions cms/server/admin/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,13 +329,16 @@ def prepare(self):
contest_id = match.group(1)
remaining_path = match.group(2) or ""

# Don't redirect question/announcement/message actions - they should use
# the contest handlers directly since questions/announcements
# belong to the managing contest, and messages use the contest user
# Don't redirect certain actions - they should use the contest handlers
# directly since questions/announcements belong to the managing contest,
# messages use the contest user, and overview/resourceslist are contest-
# specific pages that training programs redirect to
if (
remaining_path.startswith("/question/")
or remaining_path.startswith("/announcement/")
or remaining_path.endswith("/message")
or remaining_path == "/overview"
or remaining_path == "/resourceslist"
):
return

Expand Down
38 changes: 30 additions & 8 deletions cms/server/admin/handlers/contesttask.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,28 @@ def get(self, contest_id):
self.r_params["is_training_day"] = training_day is not None

if training_day is not None:
# For training days, show tasks from the training program's
# managing contest that are not already assigned to any training day
# For training days, show all tasks that are not already assigned
# to any training day. Tasks in the training program are shown first,
# followed by tasks not in the training program.
training_program = training_day.training_program
self.r_params["unassigned_tasks"] = \
self.sql_session.query(Task)\
.filter(Task.contest_id == training_program.managing_contest_id)\
.filter(Task.training_day_id.is_(None))\
.order_by(Task.num)\
.all()

# Tasks in the training program (not assigned to any training day)
program_tasks = self.sql_session.query(Task)\
.filter(Task.contest_id == training_program.managing_contest_id)\
.filter(Task.training_day_id.is_(None))\
.order_by(Task.num)\
.all()

# All other tasks (not in any contest or training day)
other_tasks = self.sql_session.query(Task)\
.filter(Task.contest_id.is_(None))\
.filter(Task.training_day_id.is_(None))\
.order_by(Task.name)\
.all()

self.r_params["unassigned_tasks"] = program_tasks + other_tasks
# Track which task IDs are in the training program for the template
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)
Expand Down Expand Up @@ -282,6 +295,15 @@ def post(self, contest_id):
task = self.safe_get_item(Task, task_id)

if training_day is not None:
training_program = training_day.training_program

# Check if task is not in the training program
if task.contest_id != training_program.managing_contest_id:
# Add the task to the training program's managing contest first
managing_contest = training_program.managing_contest
task.num = len(managing_contest.tasks)
task.contest = managing_contest

# Assign the task to the training day.
# Task keeps its contest_id (managing contest) and gets training_day_id set.
# Use training_day_num for ordering within the training day.
Expand Down
7 changes: 6 additions & 1 deletion cms/server/admin/handlers/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ class TaskHandler(BaseHandler):
@require_permission(BaseHandler.AUTHENTICATED)
def get(self, task_id):
task = self.safe_get_item(Task, task_id)
self.contest = task.contest
# If the task is assigned to an active training day (not archived),
# show the training day's contest sidebar instead of the training program sidebar
if task.training_day is not None and task.training_day.contest is not None:
self.contest = task.training_day.contest
else:
self.contest = task.contest

self.r_params = self.render_params()
self.r_params["task"] = task
Expand Down
124 changes: 100 additions & 24 deletions cms/server/admin/handlers/trainingprogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ def get(self, training_program_id: str):
def post(self, training_program_id: str):
fallback = self.url("training_program", training_program_id)
training_program = self.safe_get_item(TrainingProgram, training_program_id)
contest = training_program.managing_contest

try:
# Update training program attributes
attrs = training_program.get_attrs()
self.get_string(attrs, "name")
self.get_string(attrs, "description")
Expand All @@ -122,28 +124,72 @@ def post(self, training_program_id: str):
training_program.set_attrs(attrs)

# Sync description to managing contest
training_program.managing_contest.description = attrs["description"]

# Parse and update start/stop times on managing contest
start_str = self.get_argument("start", "")
stop_str = self.get_argument("stop", "")

if start_str:
training_program.managing_contest.start = dt.strptime(
start_str, "%Y-%m-%dT%H:%M"
)

if stop_str:
training_program.managing_contest.stop = dt.strptime(
stop_str, "%Y-%m-%dT%H:%M"
)
contest.description = attrs["description"]

# Update managing contest configuration fields
contest_attrs = contest.get_attrs()

# Allowed localizations (comma-separated list)
allowed_localizations: str = self.get_argument("allowed_localizations", "")
if allowed_localizations:
contest_attrs["allowed_localizations"] = [
x.strip()
for x in allowed_localizations.split(",")
if len(x) > 0 and not x.isspace()
]
else:
contest_attrs["allowed_localizations"] = []

# Programming languages
contest_attrs["languages"] = self.get_arguments("languages")

# Boolean settings
self.get_bool(contest_attrs, "submissions_download_allowed")
self.get_bool(contest_attrs, "allow_questions")
self.get_bool(contest_attrs, "allow_user_tests")
self.get_bool(contest_attrs, "allow_unofficial_submission_before_analysis_mode")
self.get_bool(contest_attrs, "allow_delay_requests")

# Login section boolean settings
self.get_bool(contest_attrs, "block_hidden_participations")
self.get_bool(contest_attrs, "allow_password_authentication")
self.get_bool(contest_attrs, "allow_registration")
self.get_bool(contest_attrs, "ip_restriction")
self.get_bool(contest_attrs, "ip_autologin")

# Score precision
self.get_int(contest_attrs, "score_precision")

# Times
self.get_datetime(contest_attrs, "start")
self.get_datetime(contest_attrs, "stop")
self.get_string(contest_attrs, "timezone", empty=None)
self.get_timedelta_sec(contest_attrs, "per_user_time")

# Limits
self.get_int(contest_attrs, "max_submission_number")
self.get_int(contest_attrs, "max_user_test_number")
self.get_timedelta_sec(contest_attrs, "min_submission_interval")
self.get_timedelta_sec(contest_attrs, "min_submission_interval_grace_period")
self.get_timedelta_sec(contest_attrs, "min_user_test_interval")

# Token parameters
self.get_string(contest_attrs, "token_mode")
self.get_int(contest_attrs, "token_max_number")
self.get_timedelta_sec(contest_attrs, "token_min_interval")
self.get_int(contest_attrs, "token_gen_initial")
self.get_int(contest_attrs, "token_gen_number")
self.get_timedelta_min(contest_attrs, "token_gen_interval")
self.get_int(contest_attrs, "token_gen_max")

# Apply contest attributes
contest.set_attrs(contest_attrs)

# Validate that stop is not before start (only if both are set)
if (
training_program.managing_contest.start is not None
and training_program.managing_contest.stop is not None
and training_program.managing_contest.stop
< training_program.managing_contest.start
contest.start is not None
and contest.stop is not None
and contest.stop < contest.start
):
raise ValueError("End time must be after start time")

Expand All @@ -154,7 +200,9 @@ def post(self, training_program_id: str):
self.redirect(fallback)
return

self.try_commit()
if self.try_commit():
# Update the contest on RWS.
self.service.proxy_service.reinitialize()
self.redirect(fallback)


Expand Down Expand Up @@ -440,7 +488,7 @@ def post(self, training_program_id: str):

try:
user_id: str = self.get_argument("user_id")
assert user_id != "null", "Please select a valid user"
assert user_id != "", "Please select a valid user"
except Exception as error:
self.service.add_notification(
make_datetime(), "Invalid field(s)", repr(error))
Expand All @@ -449,7 +497,13 @@ def post(self, training_program_id: str):

user = self.safe_get_item(User, user_id)

participation = Participation(contest=managing_contest, user=user)
# Set starting_time to now so the student can see everything immediately
# (training programs don't have a start button)
participation = Participation(
contest=managing_contest,
user=user,
starting_time=make_datetime()
)
self.sql_session.add(participation)
self.sql_session.flush()

Expand Down Expand Up @@ -1841,7 +1895,7 @@ def post(self, training_program_id: str, user_id: str):

try:
task_id = self.get_argument("task_id")
if task_id == "null":
if task_id in ("", "null"):
raise ValueError("Please select a task")

task = self.safe_get_item(Task, task_id)
Expand Down Expand Up @@ -1973,7 +2027,7 @@ def post(self, training_program_id: str):

try:
task_id = self.get_argument("task_id")
if task_id == "null":
if task_id in ("", "null"):
raise ValueError("Please select a task")

tag_name = self.get_argument("tag", "").strip().lower()
Expand Down Expand Up @@ -2812,3 +2866,25 @@ def get_training_day_num(task_id: int) -> tuple[int, int]:
.filter(Question.ignored.is_(False))\
.count()
self.render("training_program_combined_ranking_detail.html", **self.r_params)


class TrainingProgramOverviewRedirectHandler(BaseHandler):
"""Redirect /training_program/{id}/overview to the managing contest's overview page."""

@require_permission(BaseHandler.AUTHENTICATED)
def get(self, training_program_id: str):
training_program = self.safe_get_item(TrainingProgram, training_program_id)
self.redirect(
self.url("contest", training_program.managing_contest.id, "overview")
)


class TrainingProgramResourcesListRedirectHandler(BaseHandler):
"""Redirect /training_program/{id}/resourceslist to the managing contest's resourceslist page."""

@require_permission(BaseHandler.AUTHENTICATED)
def get(self, training_program_id: str):
training_program = self.safe_get_item(TrainingProgram, training_program_id)
self.redirect(
self.url("contest", training_program.managing_contest.id, "resourceslist")
)
41 changes: 36 additions & 5 deletions cms/server/admin/handlers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import re

from sqlalchemy import and_, exists
from cms.db import Contest, Participation, Submission, Team, User
from cms.db import Contest, Participation, Submission, Team, User, TrainingDay, TrainingProgram
from cms.server.util import exclude_internal_contests
from cmscommon.crypto import (parse_authentication,
hash_password, validate_password_strength)
Expand All @@ -48,10 +48,35 @@ def get(self, user_id):

self.r_params = self.render_params()
self.r_params["user"] = user
self.r_params["participations"] = \
self.sql_session.query(Participation)\
.filter(Participation.user == user)\
.all()

# Get all participations and separate them into categories
all_participations = self.sql_session.query(Participation)\
.filter(Participation.user == user)\
.all()

# Separate participations into:
# 1. Training program participations (managing contest)
# 2. Training day participations (hidden from list)
# 3. Regular contest participations
training_program_participations = []
regular_participations = []

for p in all_participations:
# Check if this is a training program's managing contest
if p.contest.training_program is not None:
training_program_participations.append(p)
# Check if this is a training day contest (hide from list)
elif p.contest.training_day is not None:
# Skip training day participations - they're managed via training program
pass
else:
regular_participations.append(p)

self.r_params["participations"] = regular_participations
self.r_params["training_program_participations"] = training_program_participations

# Filter out training day contests and managing contests from unassigned list
# (users should be added to training programs via the training program UI)
self.r_params["unassigned_contests"] = exclude_internal_contests(
self.sql_session.query(Contest).filter(
~exists().where(
Expand All @@ -60,6 +85,12 @@ def get(self, user_id):
Participation.user == user
)
)
).filter(
# Exclude training day contests
~exists().where(TrainingDay.contest_id == Contest.id)
).filter(
# Exclude managing contests (training program contests)
~exists().where(TrainingProgram.managing_contest_id == Contest.id)
)
).all()
self.render("user.html", **self.r_params)
Expand Down
Loading
Loading