Coverage for cc_modules/cc_membership.py: 90%

1"""

2 camcops_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

30 import logging

31 from typing import Optional

32

33 from cardinal_pythonlib.logs import BraceStyleAdapter

34 from sqlalchemy.orm import (

35 Mapped,

36 mapped_column,

37 relationship,

38 Session as SqlASession,

39)

40 from sqlalchemy.sql.schema import ForeignKey

41

42 from camcops_server.cc_modules.cc_sqlalchemy import Base

43

44 log = 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

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