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 <ackie.h.gmai@gmail.com>
# Copyright © 2016 Amir Keivan Mohtashami <akmohtashami97@gmail.com>
# Copyright © 2018 William Di Luigi <williamdiluigi@gmail.com>
# Copyright © 2026 Tobias Lenz <t_lenz94@web.de>
#
# 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
77 changes: 22 additions & 55 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 <boscarim@hotmail.com>
# Copyright © 2012-2018 Luca Wehrstedt <luca.wehrstedt@gmail.com>
# Copyright © 2013 Bernard Blackham <bernard@largestprime.net>
# Copyright © 2015 Fabian Gundlach <320pointsguy@gmail.com>
# Copyright © 2016 Myungwoo Chun <mc.tamaki@gmail.com>
# Copyright © 2016 Amir Keivan Mohtashami <akmohtashami97@gmail.com>
# Copyright © 2017-2026 Tobias Lenz <t_lenz94@web.de>
# Copyright © 2018 William Di Luigi <williamdiluigi@gmail.com>
#
# 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,8 +246,25 @@ class Contest(Base):
nullable=False,
default=0)

# Main group (id and Group object) of this contest
main_group_id: int = Column(
Integer,
ForeignKey("groups.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)

# These one-to-many relationships are the reversed directions of
# the ones defined in the "child" classes using foreign keys.
groups : list["Group"] = relationship(
"Group",
foreign_keys="[Group.contest_id]",
cascade="all, delete-orphan",
passive_deletes=True,
back_populates="contest")

tasks: list["Task"] = relationship(
"Task",
Expand All @@ -289,34 +281,9 @@ class Contest(Base):
passive_deletes=True,
back_populates="contest")

participations: list["Participation"] = relationship(
"Participation",
cascade="all, delete-orphan",
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
@property
def participations(self) -> list["Participation"]:
return sum((g.participations for g in self.groups), start=[])


class Announcement(Base):
Expand Down
122 changes: 117 additions & 5 deletions 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 <boscarim@hotmail.com>
# Copyright © 2012-2018 Luca Wehrstedt <luca.wehrstedt@gmail.com>
# Copyright © 2015 William Di Luigi <williamdiluigi@gmail.com>
# Copyright © 2015 Fabian Gundlach <320pointsguy@gmail.com>
# Copyright © 2016 Myungwoo Chun <mc.tamaki@gmail.com>
# Copyright © 2017-2026 Tobias Lenz <t_lenz94@web.de>
# Copyright © 2021 Manuel Gundlach <manuel.gundlach@gmail.com>
#
# 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 @@ -31,7 +34,7 @@
from sqlalchemy.dialects.postgresql import ARRAY, CIDR
from sqlalchemy.orm import relationship
from sqlalchemy.schema import Column, ForeignKey, CheckConstraint, \
UniqueConstraint
UniqueConstraint, ForeignKeyConstraint
from sqlalchemy.types import Boolean, Integer, String, Unicode, DateTime, \
Interval

Expand All @@ -41,6 +44,102 @@
if typing.TYPE_CHECKING:
from . import Submission, UserTest


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

"""
__tablename__ = 'groups'
__table_args__ = (
UniqueConstraint('contest_id', 'name'),
UniqueConstraint('id', 'contest_id'),
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=True,
index=True)
contest: Contest = relationship(
Contest,
foreign_keys=[contest_id],
back_populates="groups")

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

participations: list["Participation"] = relationship(
"Participation",
cascade="all, delete-orphan",
passive_deletes=True,
foreign_keys="[Participation.group_id]",
back_populates="group")


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

Expand Down Expand Up @@ -215,9 +314,7 @@ class Participation(Base):
onupdate="CASCADE", ondelete="CASCADE"),
nullable=False,
index=True)
contest: Contest = relationship(
Contest,
back_populates="participations")
contest: Contest = relationship(Contest)

# User (id and object) which is participating.
user_id: int = Column(
Expand All @@ -229,7 +326,22 @@ class Participation(Base):
user: User = relationship(
User,
back_populates="participations")
__table_args__ = (UniqueConstraint("contest_id", "user_id"),)

# Group this user belongs to
group_id: int = Column(
Integer,
ForeignKey(Group.id,
onupdate="CASCADE", ondelete="CASCADE"),
nullable=False,
index=True)
group: Group = relationship(
Group,
foreign_keys="[Participation.group_id]",
back_populates="participations")
__table_args__ = (
ForeignKeyConstraint(["group_id", "contest_id"], ["groups.id", "groups.contest_id"]),
UniqueConstraint("contest_id", "user_id")
)

# Team (id and object) that the user is representing with this
# participation.
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
14 changes: 13 additions & 1 deletion cms/server/admin/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@

from cms import __version__, config
from cms.db import Admin, Contest, Participation, Question, Submission, \
SubmissionResult, Task, Team, User, UserTest
SubmissionResult, Task, Team, User, UserTest, Group
import cms.db
from cms.grading.scoretypes import get_score_type_class
from cms.grading.tasktypes import get_task_type_class
Expand Down Expand Up @@ -659,6 +659,18 @@ def get_login_url(self) -> str:
"""
return self.url("login")

def get_group_settings(self, g: Group):
attrs = dict()
self.get_datetime(attrs, "start")
assert attrs.get("start") is not None, "No main group start time specified."
self.get_datetime(attrs, "stop")
assert attrs.get("stop") is not None, "No main group stop time specified."
self.get_timedelta_sec(attrs, "per_user_time")

self.get_bool(attrs, "analysis_enabled")
self.get_datetime(attrs, "analysis_start")
self.get_datetime(attrs, "analysis_stop")
g.set_attrs(attrs)

class FileHandler(BaseHandler, FileHandlerMixin):
pass
Expand Down
Loading
Loading