Coverage for cc_modules/cc_specialnote.py : 54%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_specialnote.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
12 CamCOPS is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
17 CamCOPS is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
25===============================================================================
27**Special notes that are attached, on the server, to tasks or patients.**
29"""
31from typing import List, Optional
33import cardinal_pythonlib.rnc_web as ws
34from sqlalchemy.orm import relationship, Session as SqlASession
35from sqlalchemy.sql.expression import update
36from sqlalchemy.sql.schema import Column, ForeignKey
37from sqlalchemy.sql.sqltypes import Boolean, Integer, UnicodeText
39from camcops_server.cc_modules.cc_constants import ERA_NOW
40from camcops_server.cc_modules.cc_request import CamcopsRequest
41from camcops_server.cc_modules.cc_sqla_coltypes import (
42 PendulumDateTimeAsIsoTextColType,
43 EraColType,
44 TableNameColType,
45)
46from camcops_server.cc_modules.cc_sqlalchemy import Base
47from camcops_server.cc_modules.cc_user import User
48from camcops_server.cc_modules.cc_xml import (
49 make_xml_branches_from_columns,
50 XmlElement,
51)
54# =============================================================================
55# SpecialNote class
56# =============================================================================
58SPECIALNOTE_FWD_REF = "SpecialNote"
61class SpecialNote(Base):
62 """
63 Represents a special note, attached server-side to a task or patient.
65 "Task" means all records representing versions of a single task instance,
66 identified by the combination of {id, device, era}.
67 """
68 __tablename__ = "_special_notes"
70 # PK:
71 note_id = Column(
72 "note_id", Integer,
73 primary_key=True, autoincrement=True,
74 comment="Arbitrary primary key"
75 )
76 # Composite FK:
77 basetable = Column(
78 "basetable", TableNameColType,
79 index=True,
80 comment="Base table of task concerned (part of FK)"
81 )
82 task_id = Column(
83 "task_id", Integer,
84 index=True,
85 comment="Client-side ID of the task, or patient, concerned "
86 "(part of FK)"
87 )
88 device_id = Column(
89 "device_id", Integer,
90 index=True,
91 comment="Source tablet device (part of FK)"
92 )
93 era = Column(
94 "era", EraColType,
95 index=True,
96 comment="Era (part of FK)"
97 )
98 # Details of note
99 note_at = Column(
100 "note_at", PendulumDateTimeAsIsoTextColType,
101 comment="Date/time of note entry (ISO 8601)"
102 )
103 user_id = Column(
104 "user_id", Integer,
105 ForeignKey("_security_users.id"),
106 comment="User that entered this note"
107 )
108 user = relationship("User")
109 note = Column(
110 "note", UnicodeText,
111 comment="Special note, added manually"
112 )
113 hidden = Column(
114 "hidden", Boolean, nullable=False, default=False,
115 comment="Manually hidden (effectively: deleted)"
116 )
118 def get_note_as_string(self) -> str:
119 """
120 Return a string-formatted version of the note.
121 """
122 return (
123 f"[{self.note_at or '?'}, "
124 f"{self.get_username() or '?'}]\n"
125 f"{self.note or ''}"
126 )
128 def get_note_as_html(self) -> str:
129 """
130 Return an HTML-formatted version of the note.
131 """
132 return (
133 f"[{self.note_at or '?'}, {self.get_username() or '?'}]<br>"
134 f"<b>{ws.webify(self.note) or ''}</b>"
135 )
137 def get_username(self) -> Optional[str]:
138 if self.user is None:
139 return None
140 return self.user.username
142 def get_xml_root(self, skip_fields: List[str] = None) -> XmlElement:
143 """
144 Get root of XML tree, as an
145 :class:`camcops_server.cc_modules.cc_xml.XmlElement`.
146 """
147 branches = make_xml_branches_from_columns(
148 self, skip_fields=skip_fields)
149 return XmlElement(name=self.__tablename__, value=branches)
151 @classmethod
152 def forcibly_preserve_special_notes_for_device(cls, req: CamcopsRequest,
153 device_id: int) -> None:
154 """
155 Force-preserve all special notes for a given device.
157 WRITES TO DATABASE.
159 For update methods, see also:
160 https://docs.sqlalchemy.org/en/latest/orm/persistence_techniques.html
161 """
162 dbsession = req.dbsession
163 new_era = req.now_era_format
165 # METHOD 1: use the ORM, object by object
166 #
167 # noinspection PyProtectedMember
168 # notes = dbsession.query(cls)\
169 # .filter(cls._device_id == device_id)\
170 # .filter(cls._era == ERA_NOW)\
171 # .all()
172 # for note in notes:
173 # note._era = new_era
175 # METHOD 2: use the Core, in bulk
176 # You can use update(table)... or table.update()...;
177 # http://docs.sqlalchemy.org/en/latest/core/dml.html#sqlalchemy.sql.expression.update # noqa
179 # noinspection PyUnresolvedReferences
180 dbsession.execute(
181 update(cls.__table__)
182 .where(cls.device_id == device_id)
183 .where(cls.era == ERA_NOW)
184 .values(era=new_era)
185 )
187 @classmethod
188 def get_specialnote_by_id(cls, dbsession: SqlASession,
189 note_id: int) -> Optional["SpecialNote"]:
190 """
191 Returns a special note, given its ID.
192 """
193 return (
194 dbsession.query(cls)
195 .filter(cls.note_id == note_id)
196 .first()
197 )
199 def get_group_id_of_target(self) -> Optional[int]:
200 """
201 Returns the group ID for the object (task or patient) that this
202 special note is about.
203 """
204 from camcops_server.cc_modules.cc_patient import Patient
205 from camcops_server.cc_modules.cc_taskfactory import task_factory_clientkeys_no_security_checks # noqa
206 dbsession = SqlASession.object_session(self)
207 group_id = None
208 if self.basetable == Patient.__tablename__:
209 # Patient
210 patient = Patient.get_patient_by_id_device_era(
211 dbsession=dbsession,
212 client_id=self.task_id,
213 device_id=self.device_id,
214 era=self.era
215 )
216 if patient:
217 group_id = patient.group_id
218 else:
219 # Task
220 task = task_factory_clientkeys_no_security_checks(
221 dbsession=dbsession,
222 basetable=self.basetable,
223 client_id=self.task_id,
224 device_id=self.device_id,
225 era=self.era
226 )
227 if task:
228 group_id = task.group_id
229 return group_id
231 def user_may_delete_specialnote(self, user: "User") -> bool:
232 """
233 May the specified user delete this note?
234 """
235 if user.superuser:
236 # Superuser can delete anything
237 return True
238 if self.user_id == user.id:
239 # Created by the current user, therefore deletable by them.
240 return True
241 group_id = self.get_group_id_of_target()
242 if group_id is None:
243 return False
244 # Can the current user administer the group that the task/patient
245 # belongs to? If so, they may delete the special note.
246 return user.may_administer_group(group_id)