Hide keyboard shortcuts

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 

2 

3""" 

4camcops_server/cc_modules/cc_specialnote.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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. 

16 

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. 

21 

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/>. 

24 

25=============================================================================== 

26 

27**Special notes that are attached, on the server, to tasks or patients.** 

28 

29""" 

30 

31from typing import List, Optional 

32 

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 

38 

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) 

52 

53 

54# ============================================================================= 

55# SpecialNote class 

56# ============================================================================= 

57 

58SPECIALNOTE_FWD_REF = "SpecialNote" 

59 

60 

61class SpecialNote(Base): 

62 """ 

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

64 

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" 

69 

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 ) 

117 

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 ) 

127 

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 ) 

136 

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

138 if self.user is None: 

139 return None 

140 return self.user.username 

141 

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) 

150 

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. 

156 

157 WRITES TO DATABASE. 

158 

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 

164 

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 

174 

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 

178 

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 ) 

186 

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 ) 

198 

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 

230 

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)