|
5 | 5 | import slugify |
6 | 6 | import sqlalchemy as sa |
7 | 7 | from sqlalchemy.dialects.postgresql import JSONB |
| 8 | +from sqlalchemy.orm import relationship |
8 | 9 |
|
9 | 10 | from h import pubid |
10 | 11 | from h.db import Base, mixins |
| 12 | +from h.models.user import User |
11 | 13 | from h.util.group import split_groupid |
12 | 14 |
|
13 | 15 | GROUP_NAME_MIN_LENGTH = 3 |
@@ -49,14 +51,19 @@ class GroupMembership(Base): |
49 | 51 | __table_args__ = (sa.UniqueConstraint("user_id", "group_id"),) |
50 | 52 |
|
51 | 53 | id = sa.Column("id", sa.Integer, autoincrement=True, primary_key=True) |
| 54 | + |
52 | 55 | user_id = sa.Column("user_id", sa.Integer, sa.ForeignKey("user.id"), nullable=False) |
| 56 | + user = relationship("User", back_populates="memberships") |
| 57 | + |
53 | 58 | group_id = sa.Column( |
54 | 59 | "group_id", |
55 | 60 | sa.Integer, |
56 | 61 | sa.ForeignKey("group.id", ondelete="cascade"), |
57 | 62 | nullable=False, |
58 | 63 | index=True, |
59 | 64 | ) |
| 65 | + group = relationship("Group", back_populates="memberships") |
| 66 | + |
60 | 67 | roles = sa.Column( |
61 | 68 | JSONB, |
62 | 69 | sa.CheckConstraint( |
@@ -159,12 +166,31 @@ def groupid(self, value): |
159 | 166 | self.authority_provided_id = groupid_parts["authority_provided_id"] |
160 | 167 | self.authority = groupid_parts["authority"] |
161 | 168 |
|
162 | | - # Group membership |
163 | | - members = sa.orm.relationship( |
164 | | - "User", |
165 | | - secondary="user_group", |
166 | | - backref=sa.orm.backref("groups", order_by="Group.name"), |
167 | | - ) |
| 169 | + memberships = sa.orm.relationship("GroupMembership", back_populates="group") |
| 170 | + |
| 171 | + @property |
| 172 | + def members(self) -> tuple[User, ...]: |
| 173 | + """ |
| 174 | + Return a tuple of this group's members. |
| 175 | +
|
| 176 | + This is a convenience property for when you want to access a group's |
| 177 | + members (User objects) rather than its memberships (GroupMembership |
| 178 | + objects). |
| 179 | +
|
| 180 | + This is not an SQLAlchemy relationship! SQLAlchemy emits a warning if |
| 181 | + you try to have both Group.memberships and a Group.members |
| 182 | + relationships at the same time because it can result in reads returning |
| 183 | + conflicting data and in writes causing integrity errors or unexpected |
| 184 | + inserts or deletes. See: |
| 185 | +
|
| 186 | + https://docs.sqlalchemy.org/en/20/orm/basic_relationships.html#combining-association-object-with-many-to-many-access-patterns |
| 187 | +
|
| 188 | + Since this is just a normal Python property setting or mutating it |
| 189 | + (e.g. `group.members = [...]` or `group.members.append(...)`) wouldn't |
| 190 | + be registered with SQLAlchemy and the changes wouldn't be saved to the |
| 191 | + DB. So this is a read-only property that returns an immutable tuple. |
| 192 | + """ |
| 193 | + return tuple(membership.user for membership in self.memberships) |
168 | 194 |
|
169 | 195 | scopes = sa.orm.relationship( |
170 | 196 | "GroupScope", backref="group", cascade="all, delete-orphan" |
|
0 commit comments