|
6 | 6 | # Copyright © 2010-2012 Matteo Boscariol <boscarim@hotmail.com> |
7 | 7 | # Copyright © 2012-2018 Luca Wehrstedt <luca.wehrstedt@gmail.com> |
8 | 8 | # Copyright © 2015 William Di Luigi <williamdiluigi@gmail.com> |
| 9 | +# Copyright © 2015 Fabian Gundlach <320pointsguy@gmail.com> |
9 | 10 | # Copyright © 2016 Myungwoo Chun <mc.tamaki@gmail.com> |
| 11 | +# Copyright © 2017-2026 Tobias Lenz <t_lenz94@web.de> |
| 12 | +# Copyright © 2021 Manuel Gundlach <manuel.gundlach@gmail.com> |
10 | 13 | # |
11 | 14 | # This program is free software: you can redistribute it and/or modify |
12 | 15 | # it under the terms of the GNU Affero General Public License as |
|
31 | 34 | from sqlalchemy.dialects.postgresql import ARRAY, CIDR |
32 | 35 | from sqlalchemy.orm import relationship |
33 | 36 | from sqlalchemy.schema import Column, ForeignKey, CheckConstraint, \ |
34 | | - UniqueConstraint |
| 37 | + UniqueConstraint, ForeignKeyConstraint |
35 | 38 | from sqlalchemy.types import Boolean, Integer, String, Unicode, DateTime, \ |
36 | 39 | Interval |
37 | 40 |
|
|
41 | 44 | if typing.TYPE_CHECKING: |
42 | 45 | from . import Submission, UserTest |
43 | 46 |
|
| 47 | + |
| 48 | +class Group(Base): |
| 49 | + """Class to store a group of users (for timing, etc.). |
| 50 | +
|
| 51 | + """ |
| 52 | + __tablename__ = 'groups' |
| 53 | + __table_args__ = ( |
| 54 | + UniqueConstraint('contest_id', 'name'), |
| 55 | + UniqueConstraint('id', 'contest_id'), |
| 56 | + CheckConstraint("start <= stop"), |
| 57 | + CheckConstraint("stop <= analysis_start"), |
| 58 | + CheckConstraint("analysis_start <= analysis_stop"), |
| 59 | + ) |
| 60 | + |
| 61 | + # Auto increment primary key. |
| 62 | + id: int = Column( |
| 63 | + Integer, |
| 64 | + primary_key=True) |
| 65 | + |
| 66 | + name: str = Column( |
| 67 | + Unicode, |
| 68 | + nullable=False) |
| 69 | + |
| 70 | + # Beginning and ending of the contest. |
| 71 | + start: datetime = Column( |
| 72 | + DateTime, |
| 73 | + nullable=False, |
| 74 | + default=datetime(2000, 1, 1)) |
| 75 | + stop: datetime = Column( |
| 76 | + DateTime, |
| 77 | + nullable=False, |
| 78 | + default=datetime(2100, 1, 1)) |
| 79 | + |
| 80 | + # Beginning and ending of the analysis mode for this group. |
| 81 | + analysis_enabled: bool = Column( |
| 82 | + Boolean, |
| 83 | + nullable=False, |
| 84 | + default=False) |
| 85 | + analysis_start: datetime = Column( |
| 86 | + DateTime, |
| 87 | + nullable=False, |
| 88 | + default=datetime(2100, 1, 1)) |
| 89 | + analysis_stop: datetime = Column( |
| 90 | + DateTime, |
| 91 | + nullable=False, |
| 92 | + default=datetime(2100, 1, 1)) |
| 93 | + |
| 94 | + # Max contest time for each user in seconds. |
| 95 | + per_user_time: timedelta | None = Column( |
| 96 | + Interval, |
| 97 | + CheckConstraint("per_user_time >= '0 seconds'"), |
| 98 | + nullable=True) |
| 99 | + |
| 100 | + # Contest (id and object) to which this user group belongs. |
| 101 | + contest_id: int = Column( |
| 102 | + Integer, |
| 103 | + ForeignKey(Contest.id, |
| 104 | + onupdate="CASCADE", ondelete="CASCADE"), |
| 105 | + nullable=False, |
| 106 | + index=True) |
| 107 | + contest: Contest = relationship( |
| 108 | + Contest, |
| 109 | + foreign_keys=[contest_id], |
| 110 | + back_populates="groups") |
| 111 | + |
| 112 | + def phase(self, timestamp: datetime) -> int: |
| 113 | + """Return: -1 if contest isn't started yet at time timestamp, |
| 114 | + 0 if the contest is active at time timestamp, |
| 115 | + 1 if the contest has ended but analysis mode |
| 116 | + hasn't started yet |
| 117 | + 2 if the contest has ended and analysis mode is active |
| 118 | + 3 if the contest has ended and analysis mode is disabled or |
| 119 | + has ended |
| 120 | +
|
| 121 | + timestamp: the time we are iterested in. |
| 122 | + """ |
| 123 | + # NOTE: this logic is duplicated in aws_utils.js. |
| 124 | + if timestamp < self.start: |
| 125 | + return -1 |
| 126 | + if timestamp <= self.stop: |
| 127 | + return 0 |
| 128 | + if self.analysis_enabled: |
| 129 | + if timestamp < self.analysis_start: |
| 130 | + return 1 |
| 131 | + elif timestamp <= self.analysis_stop: |
| 132 | + return 2 |
| 133 | + return 3 |
| 134 | + |
| 135 | + participations: list["Participation"] = relationship( |
| 136 | + "Participation", |
| 137 | + cascade="all, delete-orphan", |
| 138 | + passive_deletes=True, |
| 139 | + foreign_keys="[Participation.group_id]", |
| 140 | + back_populates="group") |
| 141 | + |
| 142 | + |
44 | 143 | class User(Base): |
45 | 144 | """Class to store a user. |
46 | 145 |
|
@@ -149,6 +248,12 @@ class Participation(Base): |
149 | 248 |
|
150 | 249 | """ |
151 | 250 | __tablename__ = 'participations' |
| 251 | + __table_args__ = ( |
| 252 | + ForeignKeyConstraint( |
| 253 | + ("group_id", "contest_id"), |
| 254 | + (Group.id, Group.contest_id)), |
| 255 | + UniqueConstraint("contest_id", "user_id"), |
| 256 | + ) |
152 | 257 |
|
153 | 258 | # Auto increment primary key. |
154 | 259 | id: int = Column( |
@@ -229,7 +334,18 @@ class Participation(Base): |
229 | 334 | user: User = relationship( |
230 | 335 | User, |
231 | 336 | back_populates="participations") |
232 | | - __table_args__ = (UniqueConstraint("contest_id", "user_id"),) |
| 337 | + |
| 338 | + # Group this user belongs to |
| 339 | + group_id: int = Column( |
| 340 | + Integer, |
| 341 | + ForeignKey(Group.id, |
| 342 | + onupdate="CASCADE", ondelete="CASCADE"), |
| 343 | + nullable=False, |
| 344 | + index=True) |
| 345 | + group: Group = relationship( |
| 346 | + Group, |
| 347 | + foreign_keys=[group_id], |
| 348 | + back_populates="participations") |
233 | 349 |
|
234 | 350 | # Team (id and object) that the user is representing with this |
235 | 351 | # participation. |
|
0 commit comments