Skip to content

Commit a801033

Browse files
authored
Add teams /1: database definitions (#545)
* Define user team table * Add .pep8speaks.yml config
1 parent 31919f7 commit a801033

File tree

6 files changed

+117
-34
lines changed

6 files changed

+117
-34
lines changed

.pep8speaks.yml

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pycodestyle:
2+
max-line-length: 88 # black defaults

db_engine.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
db_owner: postgres # to find out your db_owner type psql -l in the terminal
2-
# and check who is the owner of your postgres database
2+
# and check who is the owner of your postgres database

ramp-database/ramp_database/model/event.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,14 @@ def n_jobs(self):
339339
@property
340340
def n_participants(self):
341341
"""int: The number of participants to the event."""
342-
return len(self.event_teams)
342+
# Only select individual teams
343+
return len(
344+
[
345+
event_team
346+
for event_team in self.event_teams
347+
if event_team.team.is_individual
348+
]
349+
)
343350

344351

345352
class EventScoreType(Model):

ramp-database/ramp_database/model/submission.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,18 @@ class Submission(Model):
178178
test_time_cv_std = Column(Float, default=0.0)
179179
# the maximum memory size used when training/testing, in MB
180180
max_ram = Column(Float, default=0.0)
181+
# This is intentionally optional, and not a ForeignKey, to avoid DB migration
182+
# issues. This field is mostly used for audits to know which user
183+
# made a submission within a team.
184+
user_name = Column(String)
181185
# later also ramp_id
182186
UniqueConstraint(event_team_id, name, name="ts_constraint")
183187

184-
def __init__(self, name, event_team, session=None):
188+
def __init__(self, name, event_team, session=None, user_name=None):
185189
self.name = name
186190
self.event_team = event_team
187191
self.session = inspect(event_team).session
192+
self.user_name = user_name
188193
sha_hasher = hashlib.sha1()
189194
sha_hasher.update(_encode_string(self.event.name))
190195
sha_hasher.update(_encode_string(self.team.name))
+62-30
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import datetime
22

3+
from sqlalchemy import Enum
34
from sqlalchemy import Column
45
from sqlalchemy import String
56
from sqlalchemy import Integer
7+
from sqlalchemy import Boolean
68
from sqlalchemy import DateTime
79
from sqlalchemy import ForeignKey
810
from sqlalchemy.orm import backref
911
from sqlalchemy.orm import relationship
1012

13+
from sqlalchemy import sql
14+
1115
from .base import Model
1216

13-
__all__ = ["Team"]
17+
__all__ = ["Team", "UserTeam"]
1418

1519

1620
class Team(Model):
@@ -22,10 +26,8 @@ class Team(Model):
2226
The name of the team.
2327
admin : :class:`ramp_database.model.User`
2428
The admin user of the team.
25-
initiator : None or :class:`ramp_database.model.Team`, default is None
26-
The team initiating a merging.
27-
acceptor : None or :class:`ramp_database.model.Team`, default is None
28-
The team accepting a merging.
29+
is_individual : bool
30+
This team is an individual team.
2931
3032
Attributes
3133
----------
@@ -37,14 +39,6 @@ class Team(Model):
3739
The ID of the admin user.
3840
admin : :class:`ramp_database.model.User`
3941
The admin user instance.
40-
initiator_id : int
41-
The ID of the team asking for merging.
42-
initiator : :class:`ramp_database.model.Team`
43-
The team instance asking for merging.
44-
acceptor_id : int
45-
The ID of the team accepting the merging.
46-
acceptor : :class:`ramp_database.model.Team`
47-
The team instance accepting the merging.
4842
team_events : :class:`ramp_database.model.EventTeam`
4943
A back-reference to the events to which the team is enroll.
5044
"""
@@ -59,30 +53,68 @@ class Team(Model):
5953
"User", backref=backref("admined_teams", cascade="all, delete")
6054
)
6155

62-
# initiator asks for merge, acceptor accepts
63-
initiator_id = Column(Integer, ForeignKey("teams.id"), default=None)
64-
initiator = relationship(
65-
"Team", primaryjoin=("Team.initiator_id == Team.id"), uselist=False
66-
)
67-
68-
acceptor_id = Column(Integer, ForeignKey("teams.id"), default=None)
69-
acceptor = relationship(
70-
"Team", primaryjoin=("Team.acceptor_id == Team.id"), uselist=False
71-
)
72-
56+
is_individual = Column(Boolean, default=True, nullable=False)
7357
creation_timestamp = Column(DateTime, nullable=False)
7458

75-
def __init__(self, name, admin, initiator=None, acceptor=None):
59+
def __init__(self, name, admin, is_individual=True):
7660
self.name = name
7761
self.admin = admin
78-
self.initiator = initiator
79-
self.acceptor = acceptor
8062
self.creation_timestamp = datetime.datetime.utcnow()
63+
self.is_individual = is_individual
8164

8265
def __str__(self):
83-
return "Team({})".format(self.name)
66+
return f"Team({self.name})"
67+
68+
def __repr__(self):
69+
return (
70+
f"Team(name={self.name}, admin_name={self.admin.name}, "
71+
f"is_individual={self.is_individual})"
72+
)
73+
74+
75+
class UserTeam(Model):
76+
"""User to team many-to-many association table.
77+
78+
Parameters
79+
----------
80+
user_id : int
81+
The ID of the user.
82+
team_id : int
83+
The ID of the team.
84+
status: str
85+
The relationship status. One of "asked", "accepted".
86+
87+
Attributes
88+
----------
89+
id : int
90+
The ID of the table row.
91+
update_timestamp : datetime
92+
Last updated timestamp.
93+
"""
94+
95+
__tablename__ = "user_teams"
96+
97+
id = Column(Integer, primary_key=True)
98+
user_id = Column(Integer, ForeignKey("users.id"))
99+
user = relationship(
100+
"User", backref=backref("user_user_team", cascade="all, delete")
101+
)
102+
team_id = Column(Integer, ForeignKey("teams.id"))
103+
team = relationship(
104+
"Team", backref=backref("team_user_team", cascade="all, delete")
105+
)
106+
status = Column(Enum("asked", "accepted", name="status"), default="asked")
107+
update_timestamp = Column(
108+
DateTime, onupdate=sql.func.now(), server_default=sql.func.now()
109+
)
110+
111+
def __init__(self, user_id, team_id, status="asked"):
112+
self.user_id = user_id
113+
self.team_id = team_id
114+
self.status = status
84115

85116
def __repr__(self):
86-
return "Team(name={}, admin_name={}, initiator={}, acceptor={})".format(
87-
self.name, self.admin.name, self.initiator, self.acceptor
117+
return (
118+
f"UserTeam(user_id={self.user_id}, team_id={self.team_id}, "
119+
f"status='{self.status}')"
88120
)

ramp-database/ramp_database/model/tests/test_team.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ramp_utils.testing import database_config_template
88
from ramp_utils.testing import ramp_config_template
99

10+
from ramp_database.model import Team, UserTeam, User
1011
from ramp_database.model import EventTeam
1112
from ramp_database.model import Model
1213

@@ -33,9 +34,45 @@ def session_scope_module(database_connection):
3334

3435
def test_team_model(session_scope_module):
3536
team = get_team_by_name(session_scope_module, "test_user")
36-
assert re.match(r"Team\(name=.*test_user.*, admin_name=.*test_user.*\)", repr(team))
37+
expr = r"Team\(name=.*test_user.*, admin_name=.*test_user.*\, is_individual=True\)"
38+
assert re.match(expr, repr(team))
3739
assert re.match(r"Team\(.*test_user.*\)", str(team))
3840

41+
assert team.is_individual is True
42+
43+
44+
def test_user_team_model(session_scope_module):
45+
"""Test user / team association"""
46+
session = session_scope_module
47+
# Create a new user and team, so that we can remove them in the end.
48+
user = User("user-75", "user-75", "", "", "")
49+
session.add(user)
50+
session.commit()
51+
# Create a new non individual team
52+
team = Team(name="group_team_75", admin=user, is_individual=False)
53+
session.add(team)
54+
session.commit()
55+
56+
# And finally a user-team association
57+
user_team = UserTeam(team_id=team.id, user_id=user.id, status="accepted")
58+
session.add(user_team)
59+
session.commit()
60+
61+
msg = r"UserTeam\(user_id=4, team_id=4, status='accepted'\)"
62+
assert re.match(msg, repr(user_team))
63+
assert team.is_individual is False
64+
65+
# Check backrefs
66+
assert user_team.user.name == user.name
67+
assert user_team.team.name == team.name
68+
69+
# When a user is deleted, its team and user/team association are also deleted
70+
session.delete(user)
71+
session.commit()
72+
assert session.query(User).filter_by(name="user-75").first() is None
73+
assert session.query(Team).filter_by(name="group_team_75").first() is None
74+
assert session.query(UserTeam).filter_by(user_id=4).first() is None
75+
3976

4077
@pytest.mark.parametrize("backref, expected_type", [("team_events", EventTeam)])
4178
def test_event_model_backref(session_scope_module, backref, expected_type):

0 commit comments

Comments
 (0)