Coverage for cc_modules/cc_membership.py: 90%

29 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/cc_modules/cc_membership.py 

3 

4=============================================================================== 

5 

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

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

8 

9 This file is part of CamCOPS. 

10 

11 CamCOPS is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CamCOPS is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26**Represents a user's membership of a group.** 

27 

28""" 

29 

30import logging 

31from typing import Optional 

32 

33from cardinal_pythonlib.logs import BraceStyleAdapter 

34from sqlalchemy.orm import ( 

35 Mapped, 

36 mapped_column, 

37 relationship, 

38 Session as SqlASession, 

39) 

40from sqlalchemy.sql.schema import ForeignKey 

41 

42from camcops_server.cc_modules.cc_sqlalchemy import Base 

43 

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

45 

46 

47# ============================================================================= 

48# User-to-group association table 

49# ============================================================================= 

50# This is many-to-many: 

51# A user can [be in] many groups. 

52# A group can [contain] many users. 

53 

54# ----------------------------------------------------------------------------- 

55# First version: 

56# ----------------------------------------------------------------------------- 

57# https://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#many-to-many 

58# user_group_table = Table( 

59# "_security_user_group", 

60# Base.metadata, 

61# Column("user_id", Integer, ForeignKey("_security_users.id"), 

62# primary_key=True), 

63# Column("group_id", Integer, ForeignKey("_security_groups.id"), 

64# primary_key=True) 

65# ) 

66 

67 

68# ----------------------------------------------------------------------------- 

69# Second version, when we want more information in the relationship: 

70# ----------------------------------------------------------------------------- 

71# https://stackoverflow.com/questions/7417906/sqlalchemy-manytomany-secondary-table-with-additional-fields # noqa: E501 

72# ... no, association_proxy isn't quite what we want 

73# ... https://docs.sqlalchemy.org/en/latest/orm/extensions/associationproxy.html # noqa: E501 

74# https://docs.sqlalchemy.org/en/latest/orm/basic_relationships.html#association-object # noqa: E501 

75# ... yes 

76# ... ah, but that AND association_proxy: 

77# https://docs.sqlalchemy.org/en/latest/orm/extensions/associationproxy.html#simplifying-association-objects # noqa: E501 

78# ... no, not association_proxy! 

79 

80 

81class UserGroupMembership(Base): 

82 """ 

83 Represents a user's membership of a group, and associated per-group 

84 permissions. 

85 """ 

86 

87 __tablename__ = "_security_user_group" 

88 

89 # PK, so we can use this object easily on its own via the ORM. 

90 id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 

91 

92 # Many-to-many mapping between User and Group 

93 user_id: Mapped[Optional[int]] = mapped_column( 

94 ForeignKey("_security_users.id") 

95 ) 

96 group_id: Mapped[Optional[int]] = mapped_column( 

97 ForeignKey("_security_groups.id") 

98 ) 

99 

100 # User attributes that are specific to their group membership 

101 groupadmin: Mapped[Optional[bool]] = mapped_column( 

102 default=False, 

103 comment="Is the user a privileged administrator for this group?", 

104 ) 

105 may_upload: Mapped[Optional[bool]] = mapped_column( 

106 default=False, 

107 comment="May the user upload data from a tablet device?", 

108 ) 

109 may_register_devices: Mapped[Optional[bool]] = mapped_column( 

110 default=False, 

111 comment="May the user register tablet devices?", 

112 ) 

113 may_use_webviewer: Mapped[Optional[bool]] = mapped_column( 

114 default=False, 

115 comment="May the user use the web front end to view " "CamCOPS data?", 

116 ) 

117 view_all_patients_when_unfiltered: Mapped[Optional[bool]] = mapped_column( 

118 default=False, 

119 comment="When no record filters are applied, can the user see " 

120 "all records? (If not, then none are shown.)", 

121 ) 

122 may_dump_data: Mapped[Optional[bool]] = mapped_column( 

123 default=False, 

124 comment="May the user run database data dumps via the web interface?", 

125 ) 

126 may_run_reports: Mapped[Optional[bool]] = mapped_column( 

127 default=False, 

128 comment="May the user run reports via the web interface? " 

129 "(Overrides other view restrictions.)", 

130 ) 

131 may_add_notes: Mapped[Optional[bool]] = mapped_column( 

132 default=False, 

133 comment="May the user add special notes to tasks?", 

134 ) 

135 may_manage_patients: Mapped[Optional[bool]] = mapped_column( 

136 default=False, 

137 comment="May the user add/edit/delete patients?", 

138 ) 

139 may_email_patients: Mapped[Optional[bool]] = mapped_column( 

140 default=False, 

141 comment="May the user send emails to patients?", 

142 ) 

143 

144 group = relationship("Group", back_populates="user_group_memberships") 

145 user = relationship("User", back_populates="user_group_memberships") 

146 

147 @classmethod 

148 def get_ugm_by_id( 

149 cls, dbsession: SqlASession, ugm_id: Optional[int] 

150 ) -> Optional["UserGroupMembership"]: 

151 """ 

152 Fetches a :class:`UserGroupMembership` by its ID. 

153 """ 

154 if ugm_id is None: 

155 return None 

156 return dbsession.query(cls).filter(cls.id == ugm_id).first()