Coverage for cc_modules/cc_group.py: 69%
77 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_group.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28**Group definitions.**
30"""
32import logging
33from typing import List, Optional, Set
35from cardinal_pythonlib.logs import BraceStyleAdapter
36from cardinal_pythonlib.reprfunc import simple_repr
37from cardinal_pythonlib.sqlalchemy.orm_inspect import gen_columns
38from cardinal_pythonlib.sqlalchemy.orm_query import exists_orm
39from sqlalchemy.ext.associationproxy import association_proxy
40from sqlalchemy.orm import relationship, Session as SqlASession
41from sqlalchemy.sql.schema import Column, ForeignKey, Table
42from sqlalchemy.sql.sqltypes import Integer
44from camcops_server.cc_modules.cc_ipuse import IpUse
45from camcops_server.cc_modules.cc_policy import TokenizedPolicy
46from camcops_server.cc_modules.cc_sqla_coltypes import (
47 GroupDescriptionColType,
48 GroupNameColType,
49 IdPolicyColType,
50)
51from camcops_server.cc_modules.cc_sqlalchemy import Base
53log = BraceStyleAdapter(logging.getLogger(__name__))
56# =============================================================================
57# Group-to-group association table
58# =============================================================================
59# A group can always see itself, but may also have permission to see others;
60# see "Groups" in the CamCOPS documentation.
62# https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#self-referential-many-to-many-relationship # noqa
63group_group_table = Table(
64 "_security_group_group",
65 Base.metadata,
66 Column(
67 "group_id",
68 Integer,
69 ForeignKey("_security_groups.id"),
70 primary_key=True,
71 ),
72 Column(
73 "can_see_group_id",
74 Integer,
75 ForeignKey("_security_groups.id"),
76 primary_key=True,
77 ),
78)
81# =============================================================================
82# Group
83# =============================================================================
86class Group(Base):
87 """
88 Represents a CamCOPS group.
90 See "Groups" in the CamCOPS documentation.
91 """
93 __tablename__ = "_security_groups"
95 id = Column(
96 "id",
97 Integer,
98 primary_key=True,
99 autoincrement=True,
100 index=True,
101 comment="Group ID",
102 )
103 name = Column(
104 "name",
105 GroupNameColType,
106 nullable=False,
107 index=True,
108 unique=True,
109 comment="Group name",
110 )
111 description = Column(
112 "description",
113 GroupDescriptionColType,
114 comment="Description of the group",
115 )
116 upload_policy = Column(
117 "upload_policy",
118 IdPolicyColType,
119 comment="Upload policy for the group, as a string",
120 )
121 finalize_policy = Column(
122 "finalize_policy",
123 IdPolicyColType,
124 comment="Finalize policy for the group, as a string",
125 )
127 ip_use_id = Column(
128 "ip_use_id",
129 Integer,
130 ForeignKey(IpUse.id),
131 nullable=True,
132 comment=f"FK to {IpUse.__tablename__}.{IpUse.id.name}",
133 )
135 ip_use = relationship(
136 IpUse, uselist=False, single_parent=True, cascade="all, delete-orphan"
137 )
139 # users = relationship(
140 # "User", # defined with string to avoid circular import
141 # secondary=user_group_table, # link via this mapping table
142 # back_populates="groups" # see User.groups
143 # )
144 user_group_memberships = relationship(
145 "UserGroupMembership", back_populates="group"
146 )
147 users = association_proxy("user_group_memberships", "user")
149 regular_user_group_memberships = relationship(
150 "UserGroupMembership",
151 primaryjoin="and_("
152 "Group.id==UserGroupMembership.group_id, "
153 "User.id==UserGroupMembership.user_id, "
154 "User.auto_generated==False)",
155 )
156 regular_users = association_proxy("regular_user_group_memberships", "user")
158 can_see_other_groups = relationship(
159 "Group", # link back to our own class
160 secondary=group_group_table, # via this mapping table
161 primaryjoin=(id == group_group_table.c.group_id), # "us"
162 secondaryjoin=(id == group_group_table.c.can_see_group_id), # "them"
163 backref="groups_that_can_see_us",
164 lazy="joined", # not sure this does anything here
165 )
167 def __str__(self) -> str:
168 return f"Group {self.id} ({self.name})"
170 def __repr__(self) -> str:
171 attrnames = sorted(attrname for attrname, _ in gen_columns(self))
172 return simple_repr(self, attrnames)
174 def ids_of_other_groups_group_may_see(self) -> Set[int]:
175 """
176 Returns a list of group IDs for groups that this group has permission
177 to see. (Always includes our own group number.)
178 """
179 group_ids = set() # type: Set[int]
180 for other_group in self.can_see_other_groups: # type: Group
181 other_group_id = other_group.id # type: Optional[int]
182 if other_group_id is not None:
183 group_ids.add(other_group_id)
184 return group_ids
186 def ids_of_groups_group_may_see(self) -> Set[int]:
187 """
188 Returns a list of group IDs for groups that this group has permission
189 to see. (Always includes our own group number.)
190 """
191 ourself = {self.id} # type: Set[int]
192 return ourself.union(self.ids_of_other_groups_group_may_see())
194 @classmethod
195 def get_groups_from_id_list(
196 cls, dbsession: SqlASession, group_ids: List[int]
197 ) -> List["Group"]:
198 """
199 Fetches groups from a list of group IDs.
200 """
201 return dbsession.query(Group).filter(Group.id.in_(group_ids)).all()
203 @classmethod
204 def get_group_by_name(
205 cls, dbsession: SqlASession, name: str
206 ) -> Optional["Group"]:
207 """
208 Fetches a group from its name.
209 """
210 if not name:
211 return None
212 return dbsession.query(cls).filter(cls.name == name).first()
214 @classmethod
215 def get_group_by_id(
216 cls, dbsession: SqlASession, group_id: int
217 ) -> Optional["Group"]:
218 """
219 Fetches a group from its integer ID.
220 """
221 if group_id is None:
222 return None
223 return dbsession.query(cls).filter(cls.id == group_id).first()
225 @classmethod
226 def get_all_groups(cls, dbsession: SqlASession) -> List["Group"]:
227 """
228 Returns all groups.
229 """
230 return dbsession.query(Group).all()
232 @classmethod
233 def all_group_ids(cls, dbsession: SqlASession) -> List[int]:
234 """
235 Returns all group IDs.
236 """
237 query = dbsession.query(cls).order_by(cls.id)
238 return [g.id for g in query]
240 @classmethod
241 def all_group_names(cls, dbsession: SqlASession) -> List[str]:
242 """
243 Returns all group names.
244 """
245 query = dbsession.query(cls).order_by(cls.id)
246 return [g.name for g in query]
248 @classmethod
249 def group_exists(cls, dbsession: SqlASession, group_id: int) -> bool:
250 """
251 Does a particular group (specified by its integer ID) exist?
252 """
253 return exists_orm(dbsession, cls, cls.id == group_id)
255 def tokenized_upload_policy(self) -> TokenizedPolicy:
256 """
257 Returns the upload policy for a group.
258 """
259 return TokenizedPolicy(self.upload_policy)
261 def tokenized_finalize_policy(self) -> TokenizedPolicy:
262 """
263 Returns the finalize policy for a group.
264 """
265 return TokenizedPolicy(self.finalize_policy)