Coverage for cc_modules/cc_specialnote.py: 49%

84 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_specialnote.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**Special notes that are attached, on the server, to tasks or patients.** 

29 

30""" 

31 

32from typing import List, Optional, TYPE_CHECKING 

33 

34import cardinal_pythonlib.rnc_web as ws 

35from sqlalchemy.orm import relationship, Session as SqlASession 

36from sqlalchemy.sql.expression import update 

37from sqlalchemy.sql.schema import Column, ForeignKey 

38from sqlalchemy.sql.sqltypes import Boolean, Integer, UnicodeText 

39 

40from camcops_server.cc_modules.cc_constants import ERA_NOW 

41from camcops_server.cc_modules.cc_request import CamcopsRequest 

42from camcops_server.cc_modules.cc_sqla_coltypes import ( 

43 PendulumDateTimeAsIsoTextColType, 

44 EraColType, 

45 TableNameColType, 

46) 

47from camcops_server.cc_modules.cc_sqlalchemy import Base 

48from camcops_server.cc_modules.cc_user import User 

49from camcops_server.cc_modules.cc_xml import ( 

50 make_xml_branches_from_columns, 

51 XmlElement, 

52) 

53 

54if TYPE_CHECKING: 

55 from camcops_server.cc_modules.cc_patient import Patient 

56 from camcops_server.cc_modules.cc_task import Task 

57 

58 

59# ============================================================================= 

60# SpecialNote class 

61# ============================================================================= 

62 

63SPECIALNOTE_FWD_REF = "SpecialNote" 

64 

65 

66class SpecialNote(Base): 

67 """ 

68 Represents a special note, attached server-side to a task or patient. 

69 

70 "Task" means all records representing versions of a single task instance, 

71 identified by the combination of {id, device, era}. 

72 """ 

73 

74 __tablename__ = "_special_notes" 

75 

76 # PK: 

77 note_id = Column( 

78 "note_id", 

79 Integer, 

80 primary_key=True, 

81 autoincrement=True, 

82 comment="Arbitrary primary key", 

83 ) 

84 # Composite FK: 

85 basetable = Column( 

86 "basetable", 

87 TableNameColType, 

88 index=True, 

89 comment="Base table of task concerned (part of FK)", 

90 ) 

91 task_id = Column( 

92 "task_id", 

93 Integer, 

94 index=True, 

95 comment="Client-side ID of the task, or patient, concerned " 

96 "(part of FK)", 

97 ) 

98 device_id = Column( 

99 "device_id", 

100 Integer, 

101 index=True, 

102 comment="Source tablet device (part of FK)", 

103 ) 

104 era = Column("era", EraColType, index=True, comment="Era (part of FK)") 

105 # Details of note 

106 note_at = Column( 

107 "note_at", 

108 PendulumDateTimeAsIsoTextColType, 

109 comment="Date/time of note entry (ISO 8601)", 

110 ) 

111 user_id = Column( 

112 "user_id", 

113 Integer, 

114 ForeignKey("_security_users.id"), 

115 comment="User that entered this note", 

116 ) 

117 user = relationship("User") 

118 note = Column("note", UnicodeText, comment="Special note, added manually") 

119 hidden = Column( 

120 "hidden", 

121 Boolean, 

122 nullable=False, 

123 default=False, 

124 comment="Manually hidden (effectively: deleted)", 

125 ) 

126 

127 def get_note_as_string(self) -> str: 

128 """ 

129 Return a string-formatted version of the note. 

130 """ 

131 return ( 

132 f"[{self.note_at or '?'}, " 

133 f"{self.get_username() or '?'}]\n" 

134 f"{self.note or ''}" 

135 ) 

136 

137 def get_note_as_html(self) -> str: 

138 """ 

139 Return an HTML-formatted version of the note. 

140 """ 

141 return ( 

142 f"[{self.note_at or '?'}, {self.get_username() or '?'}]<br>" 

143 f"<b>{ws.webify(self.note) or ''}</b>" 

144 ) 

145 

146 def get_username(self) -> Optional[str]: 

147 if self.user is None: 

148 return None 

149 return self.user.username 

150 

151 def get_xml_root(self, skip_fields: List[str] = None) -> XmlElement: 

152 """ 

153 Get root of XML tree, as an 

154 :class:`camcops_server.cc_modules.cc_xml.XmlElement`. 

155 """ 

156 branches = make_xml_branches_from_columns( 

157 self, skip_fields=skip_fields 

158 ) 

159 return XmlElement(name=self.__tablename__, value=branches) 

160 

161 @classmethod 

162 def forcibly_preserve_special_notes_for_device( 

163 cls, req: CamcopsRequest, device_id: int 

164 ) -> None: 

165 """ 

166 Force-preserve all special notes for a given device. 

167 

168 WRITES TO DATABASE. 

169 

170 For update methods, see also: 

171 https://docs.sqlalchemy.org/en/latest/orm/persistence_techniques.html 

172 """ 

173 dbsession = req.dbsession 

174 new_era = req.now_era_format 

175 

176 # METHOD 1: use the ORM, object by object 

177 # 

178 # noinspection PyProtectedMember 

179 # notes = dbsession.query(cls)\ 

180 # .filter(cls._device_id == device_id)\ 

181 # .filter(cls._era == ERA_NOW)\ 

182 # .all() 

183 # for note in notes: 

184 # note._era = new_era 

185 

186 # METHOD 2: use the Core, in bulk 

187 # You can use update(table)... or table.update()...; 

188 # https://docs.sqlalchemy.org/en/latest/core/dml.html#sqlalchemy.sql.expression.update # noqa 

189 

190 # noinspection PyUnresolvedReferences 

191 dbsession.execute( 

192 update(cls.__table__) 

193 .where(cls.device_id == device_id) 

194 .where(cls.era == ERA_NOW) 

195 .values(era=new_era) 

196 ) 

197 

198 @classmethod 

199 def get_specialnote_by_id( 

200 cls, dbsession: SqlASession, note_id: int 

201 ) -> Optional["SpecialNote"]: 

202 """ 

203 Returns a special note, given its ID. 

204 """ 

205 return dbsession.query(cls).filter(cls.note_id == note_id).first() 

206 

207 def refers_to_patient(self) -> bool: 

208 """ 

209 Is this a note relating to a patient, rather than a task? 

210 """ 

211 from camcops_server.cc_modules.cc_patient import ( 

212 Patient, 

213 ) # delayed import 

214 

215 return self.basetable == Patient.__tablename__ 

216 

217 def refers_to_task(self) -> bool: 

218 """ 

219 Is this a note relating to a task, rather than a patient? 

220 """ 

221 return not self.refers_to_patient() 

222 

223 def target_patient(self) -> Optional["Patient"]: 

224 """ 

225 Get the patient to which this note refers, or ``None`` if it doesn't. 

226 """ 

227 from camcops_server.cc_modules.cc_patient import ( 

228 Patient, 

229 ) # delayed import 

230 

231 if not self.refers_to_patient(): 

232 return None 

233 dbsession = SqlASession.object_session(self) 

234 return Patient.get_patient_by_id_device_era( 

235 dbsession=dbsession, 

236 client_id=self.task_id, 

237 device_id=self.device_id, 

238 era=self.era, 

239 ) 

240 

241 def target_task(self) -> Optional["Task"]: 

242 """ 

243 Get the patient to which this note refers, or ``None`` if it doesn't. 

244 """ 

245 from camcops_server.cc_modules.cc_taskfactory import ( 

246 task_factory_clientkeys_no_security_checks, 

247 ) # delayed import 

248 

249 if not self.refers_to_task(): 

250 return None 

251 dbsession = SqlASession.object_session(self) 

252 return task_factory_clientkeys_no_security_checks( 

253 dbsession=dbsession, 

254 basetable=self.basetable, 

255 client_id=self.task_id, 

256 device_id=self.device_id, 

257 era=self.era, 

258 ) 

259 

260 def get_group_id_of_target(self) -> Optional[int]: 

261 """ 

262 Returns the group ID for the object (task or patient) that this 

263 special note is about. 

264 """ 

265 group_id = None 

266 if self.refers_to_patient(): 

267 # Patient 

268 patient = self.target_patient() 

269 if patient: 

270 group_id = patient.group_id 

271 else: 

272 # Task 

273 task = self.target_task() 

274 if task: 

275 group_id = task.group_id 

276 return group_id 

277 

278 def user_may_delete_specialnote(self, user: "User") -> bool: 

279 """ 

280 May the specified user delete this note? 

281 """ 

282 if user.superuser: 

283 # Superuser can delete anything 

284 return True 

285 if self.user_id == user.id: 

286 # Created by the current user, therefore deletable by them. 

287 return True 

288 group_id = self.get_group_id_of_target() 

289 if group_id is None: 

290 return False 

291 # Can the current user administer the group that the task/patient 

292 # belongs to? If so, they may delete the special note. 

293 return user.may_administer_group(group_id)