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

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_group.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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/>. 

25 

26=============================================================================== 

27 

28**Group definitions.** 

29 

30""" 

31 

32import logging 

33from typing import List, Optional, Set 

34 

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 

43 

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 

52 

53log = BraceStyleAdapter(logging.getLogger(__name__)) 

54 

55 

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. 

61 

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) 

79 

80 

81# ============================================================================= 

82# Group 

83# ============================================================================= 

84 

85 

86class Group(Base): 

87 """ 

88 Represents a CamCOPS group. 

89 

90 See "Groups" in the CamCOPS documentation. 

91 """ 

92 

93 __tablename__ = "_security_groups" 

94 

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 ) 

126 

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 ) 

134 

135 ip_use = relationship( 

136 IpUse, uselist=False, single_parent=True, cascade="all, delete-orphan" 

137 ) 

138 

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") 

148 

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") 

157 

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 ) 

166 

167 def __str__(self) -> str: 

168 return f"Group {self.id} ({self.name})" 

169 

170 def __repr__(self) -> str: 

171 attrnames = sorted(attrname for attrname, _ in gen_columns(self)) 

172 return simple_repr(self, attrnames) 

173 

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 

185 

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()) 

193 

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() 

202 

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() 

213 

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() 

224 

225 @classmethod 

226 def get_all_groups(cls, dbsession: SqlASession) -> List["Group"]: 

227 """ 

228 Returns all groups. 

229 """ 

230 return dbsession.query(Group).all() 

231 

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] 

239 

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] 

247 

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) 

254 

255 def tokenized_upload_policy(self) -> TokenizedPolicy: 

256 """ 

257 Returns the upload policy for a group. 

258 """ 

259 return TokenizedPolicy(self.upload_policy) 

260 

261 def tokenized_finalize_policy(self) -> TokenizedPolicy: 

262 """ 

263 Returns the finalize policy for a group. 

264 """ 

265 return TokenizedPolicy(self.finalize_policy)