Skip to content
Open
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
5 changes: 3 additions & 2 deletions cms/db/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# Copyright © 2016 Masaki Hara <[email protected]>
# Copyright © 2016 Amir Keivan Mohtashami <[email protected]>
# Copyright © 2018 William Di Luigi <[email protected]>
# Copyright © 2026 Tobias Lenz <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand Down Expand Up @@ -55,7 +56,7 @@
# contest
"Contest", "Announcement",
# user
"User", "Team", "Participation", "Message", "Question",
"Group", "User", "Team", "Participation", "Message", "Question",
# admin
"Admin",
# task
Expand Down Expand Up @@ -95,7 +96,7 @@
from .fsobject import FSObject, LargeObject
from .admin import Admin
from .contest import Contest, Announcement
from .user import User, Team, Participation, Message, Question
from .user import Group, User, Team, Participation, Message, Question
from .task import Task, Statement, Attachment, Dataset, Manager, Testcase
from .submission import Submission, File, Token, SubmissionResult, \
Executable, Evaluation
Expand Down
66 changes: 16 additions & 50 deletions cms/db/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
# Copyright © 2012-2018 Luca Wehrstedt <[email protected]>
# Copyright © 2013 Bernard Blackham <[email protected]>
# Copyright © 2015 Fabian Gundlach <[email protected]>
# Copyright © 2016 Myungwoo Chun <[email protected]>
# Copyright © 2016 Amir Keivan Mohtashami <[email protected]>
# Copyright © 2017-2026 Tobias Lenz <[email protected]>
# Copyright © 2018 William Di Luigi <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
Expand Down Expand Up @@ -50,9 +52,6 @@ class Contest(Base):
"""
__tablename__ = 'contests'
__table_args__ = (
CheckConstraint("start <= stop"),
CheckConstraint("stop <= analysis_start"),
CheckConstraint("analysis_start <= analysis_stop"),
CheckConstraint("token_gen_initial <= token_gen_max"),
)

Expand Down Expand Up @@ -197,30 +196,6 @@ class Contest(Base):
CheckConstraint("token_gen_max > 0"),
nullable=True)

# Beginning and ending of the contest.
start: datetime = Column(
DateTime,
nullable=False,
default=datetime(2000, 1, 1))
stop: datetime = Column(
DateTime,
nullable=False,
default=datetime(2030, 1, 1))

# Beginning and ending of the contest anaylsis mode.
analysis_enabled: bool = Column(
Boolean,
nullable=False,
default=False)
analysis_start: datetime = Column(
DateTime,
nullable=False,
default=datetime(2030, 1, 1))
analysis_stop: datetime = Column(
DateTime,
nullable=False,
default=datetime(2030, 1, 1))

# Timezone for the contest. All timestamps in CWS will be shown
# using the timezone associated to the logged-in user or (if it's
# None or an invalid string) the timezone associated to the
Expand Down Expand Up @@ -271,6 +246,20 @@ class Contest(Base):
nullable=False,
default=0)

# Main group (id and Group object) of this contest
main_group_id: int = Column(
Integer,
ForeignKey("group.id", use_alter=True, name="fk_contest_main_group_id",
onupdate="CASCADE", ondelete="SET NULL"),
index=True)
main_group = relationship(
"Group",
primaryjoin="Group.id==Contest.main_group_id",
post_update=True)

# Follows the description of the fields automatically added by
# SQLAlchemy.
# groups (list of Group objects)
# These one-to-many relationships are the reversed directions of
# the ones defined in the "child" classes using foreign keys.

Expand All @@ -295,29 +284,6 @@ class Contest(Base):
passive_deletes=True,
back_populates="contest")

def phase(self, timestamp: datetime) -> int:
"""Return: -1 if contest isn't started yet at time timestamp,
0 if the contest is active at time timestamp,
1 if the contest has ended but analysis mode
hasn't started yet
2 if the contest has ended and analysis mode is active
3 if the contest has ended and analysis mode is disabled or
has ended

timestamp: the time we are iterested in.
"""
# NOTE: this logic is duplicated in aws_utils.js.
if timestamp < self.start:
return -1
if timestamp <= self.stop:
return 0
if self.analysis_enabled:
if timestamp < self.analysis_start:
return 1
elif timestamp <= self.analysis_stop:
return 2
return 3


class Announcement(Base):
"""Class to store a messages sent by the contest managers to all
Expand Down
113 changes: 112 additions & 1 deletion cms/db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
# Copyright © 2010-2012 Matteo Boscariol <[email protected]>
# Copyright © 2012-2018 Luca Wehrstedt <[email protected]>
# Copyright © 2015 William Di Luigi <[email protected]>
# Copyright © 2015 Fabian Gundlach <[email protected]>
# Copyright © 2016 Myungwoo Chun <[email protected]>
# Copyright © 2017-2026 Tobias Lenz <[email protected]>
# Copyright © 2021 Manuel Gundlach <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -29,7 +32,7 @@
from ipaddress import IPv4Network, IPv6Network

from sqlalchemy.dialects.postgresql import ARRAY, CIDR
from sqlalchemy.orm import relationship
from sqlalchemy.orm import backref, relationship
from sqlalchemy.schema import Column, ForeignKey, CheckConstraint, \
UniqueConstraint
from sqlalchemy.types import Boolean, Integer, String, Unicode, DateTime, \
Expand All @@ -41,6 +44,101 @@
if typing.TYPE_CHECKING:
from . import Submission, UserTest


class Group(Base):
"""Class to store a group of users (for timing, etc.).

"""
__tablename__ = 'group'
__table_args__ = (
UniqueConstraint('contest_id', 'name'),
CheckConstraint("start <= stop"),
CheckConstraint("stop <= analysis_start"),
CheckConstraint("analysis_start <= analysis_stop"),
)

# Auto increment primary key.
id: int = Column(
Integer,
primary_key=True)

name: str = Column(
Unicode,
nullable=False)

# Beginning and ending of the contest.
start: datetime = Column(
DateTime,
nullable=False,
default=datetime(2000, 1, 1))
stop: datetime = Column(
DateTime,
nullable=False,
default=datetime(2100, 1, 1))

# Beginning and ending of the analysis mode for this group.
analysis_enabled: bool = Column(
Boolean,
nullable=False,
default=False)
analysis_start: datetime = Column(
DateTime,
nullable=False,
default=datetime(2100, 1, 1))
analysis_stop: datetime = Column(
DateTime,
nullable=False,
default=datetime(2100, 1, 1))

# Max contest time for each user in seconds.
per_user_time: timedelta | None = Column(
Interval,
CheckConstraint("per_user_time >= '0 seconds'"),
nullable=True)

# Contest (id and object) to which this user group belongs.
contest_id: int = Column(
Integer,
ForeignKey(Contest.id,
onupdate="CASCADE", ondelete="CASCADE"),
# nullable=False,
index=True)
contest: Contest = relationship(
Contest,
backref=backref('groups',
cascade="all, delete-orphan",
passive_deletes=True),
primaryjoin="Contest.id==Group.contest_id")

def phase(self, timestamp: datetime) -> int:
"""Return: -1 if contest isn't started yet at time timestamp,
0 if the contest is active at time timestamp,
1 if the contest has ended but analysis mode
hasn't started yet
2 if the contest has ended and analysis mode is active
3 if the contest has ended and analysis mode is disabled or
has ended

timestamp (datetime): the time we are iterested in.
return (int): contest phase as above.

"""
if timestamp < self.start:
return -1
if timestamp <= self.stop:
return 0
if self.analysis_enabled:
if timestamp < self.analysis_start:
return 1
elif timestamp <= self.analysis_stop:
return 2
return 3

# Follows the description of the fields automatically added by
# SQLAlchemy.
# participations (list of Participation objects)


class User(Base):
"""Class to store a user.

Expand Down Expand Up @@ -231,6 +329,19 @@ class Participation(Base):
back_populates="participations")
__table_args__ = (UniqueConstraint("contest_id", "user_id"),)

# Group this user belongs to
group_id = Column(
Integer,
ForeignKey(Group.id,
onupdate="CASCADE", ondelete="CASCADE"),
nullable=False,
index=True)
group = relationship(
Group,
backref=backref("participations",
cascade="all, delete-orphan",
passive_deletes=True))

# Team (id and object) that the user is representing with this
# participation.
team_id: int | None = Column(
Expand Down
11 changes: 10 additions & 1 deletion cms/server/admin/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@
AddTeamHandler, \
TeamHandler, \
TeamListHandler, \
RemoveTeamHandler
RemoveTeamHandler, \
GroupListHandler, \
AddGroupHandler, \
GroupHandler
from .usertest import \
UserTestHandler, \
UserTestFileHandler
Expand Down Expand Up @@ -136,6 +139,12 @@
(r"/contest/([0-9]+)/user/([0-9]+)/edit", ParticipationHandler),
(r"/contest/([0-9]+)/user/([0-9]+)/message", MessageHandler),

# Contest's groups

(r"/contest/([0-9]+)/groups", GroupListHandler),
(r"/contest/([0-9]+)/groups/add", AddGroupHandler),
(r"/contest/([0-9]+)/group/([0-9]+)/edit", GroupHandler),

# Contest's tasks

(r"/contest/([0-9]+)/tasks", ContestTasksHandler),
Expand Down
24 changes: 16 additions & 8 deletions cms/server/admin/handlers/contest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# Copyright © 2016 Myungwoo Chun <[email protected]>
# Copyright © 2016 Amir Keivan Mohtashami <[email protected]>
# Copyright © 2018 William Di Luigi <[email protected]>
# Copyright © 2026 Tobias Lenz <[email protected]>
# Copyright © 2026 Chuyang Wang <[email protected]>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
Expand All @@ -29,7 +31,7 @@
"""

from cms import ServiceCoord, get_service_shards, get_service_address
from cms.db import Contest, Participation, Submission
from cms.db import Contest, Participation, Group, Submission
from cmscommon.datetime import make_datetime

from .base import BaseHandler, SimpleContestHandler, SimpleHandler, \
Expand All @@ -54,6 +56,12 @@ def post(self):

# Create the contest.
contest = Contest(**attrs)

# Add the default group
group = Group(name="default")
contest.groups.append(group)
contest.main_group = group

self.sql_session.add(contest)

except Exception as error:
Expand Down Expand Up @@ -117,16 +125,16 @@ def post(self, contest_id: str):
self.get_timedelta_sec(attrs, "min_submission_interval_grace_period")
self.get_timedelta_sec(attrs, "min_user_test_interval")

self.get_datetime(attrs, "start")
self.get_datetime(attrs, "stop")

self.get_string(attrs, "timezone", empty=None)
self.get_timedelta_sec(attrs, "per_user_time")
self.get_int(attrs, "score_precision")

self.get_bool(attrs, "analysis_enabled")
self.get_datetime(attrs, "analysis_start")
self.get_datetime(attrs, "analysis_stop")
main_group_attrs = dict()
self.get_datetime(main_group_attrs, "main_group_start")
assert main_group_attrs.get("main_group_start") is not None, "No main group start time specified."
self.get_datetime(main_group_attrs, "main_group_stop")
assert main_group_attrs.get("main_group_stop") is not None, "No main group stop time specified."
contest.main_group.start = main_group_attrs.get("main_group_start")
contest.main_group.stop = main_group_attrs.get("main_group_stop")

# Update the contest.
contest.set_attrs(attrs)
Expand Down
Loading
Loading