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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/cc_modules/cc_membership.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26**Represents a user's membership of a group.**
28"""
30import logging
31from typing import Optional
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
42from camcops_server.cc_modules.cc_sqlalchemy import Base
44log = BraceStyleAdapter(logging.getLogger(__name__))
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.
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# )
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!
81class UserGroupMembership(Base):
82 """
83 Represents a user's membership of a group, and associated per-group
84 permissions.
85 """
87 __tablename__ = "_security_user_group"
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)
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 )
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 )
144 group = relationship("Group", back_populates="user_group_memberships")
145 user = relationship("User", back_populates="user_group_memberships")
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()