Coverage for tasks/photo.py: 64%

85 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/tasks/photo.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""" 

29 

30from typing import List, Optional, Type 

31 

32import cardinal_pythonlib.rnc_web as ws 

33from sqlalchemy.sql.schema import Column 

34from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

35 

36from camcops_server.cc_modules.cc_blob import ( 

37 Blob, 

38 blob_relationship, 

39 get_blob_img_html, 

40) 

41from camcops_server.cc_modules.cc_constants import CssClass 

42from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

43from camcops_server.cc_modules.cc_db import ( 

44 ancillary_relationship, 

45 GenericTabletRecordMixin, 

46 TaskDescendant, 

47) 

48from camcops_server.cc_modules.cc_html import answer, tr_qa 

49from camcops_server.cc_modules.cc_request import CamcopsRequest 

50from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

51from camcops_server.cc_modules.cc_sqla_coltypes import CamcopsColumn 

52from camcops_server.cc_modules.cc_sqlalchemy import Base 

53from camcops_server.cc_modules.cc_task import ( 

54 Task, 

55 TaskHasClinicianMixin, 

56 TaskHasPatientMixin, 

57) 

58 

59 

60# ============================================================================= 

61# Photo 

62# ============================================================================= 

63 

64 

65class Photo(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

66 """ 

67 Server implementation of the Photo task. 

68 """ 

69 

70 __tablename__ = "photo" 

71 shortname = "Photo" 

72 info_filename_stem = "clinical" 

73 

74 description = Column( 

75 "description", UnicodeText, comment="Description of the photograph" 

76 ) 

77 photo_blobid = CamcopsColumn( 

78 "photo_blobid", 

79 Integer, 

80 is_blob_id_field=True, 

81 blob_relationship_attr_name="photo", 

82 comment="ID of the BLOB (foreign key to blobs.id, given " 

83 "matching device and current/frozen record status)", 

84 ) 

85 # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE: 

86 rotation = Column( # DEFUNCT as of v2.0.0 

87 "rotation", 

88 Integer, 

89 comment="Rotation (clockwise, in degrees) to be applied for viewing", 

90 ) 

91 

92 photo = blob_relationship("Photo", "photo_blobid") # type: Optional[Blob] 

93 

94 @staticmethod 

95 def longname(req: "CamcopsRequest") -> str: 

96 _ = req.gettext 

97 return _("Photograph") 

98 

99 def is_complete(self) -> bool: 

100 return self.photo_blobid is not None 

101 

102 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

103 if not self.is_complete(): 

104 return CTV_INCOMPLETE 

105 if not self.description: 

106 return [] 

107 return [CtvInfo(content=self.description)] 

108 

109 def get_task_html(self, req: CamcopsRequest) -> str: 

110 # noinspection PyTypeChecker 

111 return """ 

112 <table class="{CssClass.TASKDETAIL}"> 

113 <tr class="{CssClass.SUBHEADING}"><td>Description</td></tr> 

114 <tr><td>{description}</td></tr> 

115 <tr class="{CssClass.SUBHEADING}"><td>Photo</td></tr> 

116 <tr><td>{photo}</td></tr> 

117 </table> 

118 """.format( 

119 CssClass=CssClass, 

120 description=answer( 

121 ws.webify(self.description), 

122 default="(No description)", 

123 default_for_blank_strings=True, 

124 ), 

125 # ... xhtml2pdf crashes if the contents are empty... 

126 photo=get_blob_img_html(self.photo), 

127 ) 

128 

129 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

130 if not self.is_complete(): 

131 return [] 

132 return [ 

133 SnomedExpression(req.snomed(SnomedLookup.PHOTOGRAPH_PROCEDURE)), 

134 SnomedExpression( 

135 req.snomed(SnomedLookup.PHOTOGRAPH_PHYSICAL_OBJECT) 

136 ), 

137 ] 

138 

139 

140# ============================================================================= 

141# PhotoSequence 

142# ============================================================================= 

143 

144 

145class PhotoSequenceSinglePhoto(GenericTabletRecordMixin, TaskDescendant, Base): 

146 __tablename__ = "photosequence_photos" 

147 

148 photosequence_id = Column( 

149 "photosequence_id", 

150 Integer, 

151 nullable=False, 

152 comment="Tablet FK to photosequence", 

153 ) 

154 seqnum = Column( 

155 "seqnum", 

156 Integer, 

157 nullable=False, 

158 comment="Sequence number of this photo " 

159 "(consistently 1-based as of 2018-12-01)", 

160 ) 

161 description = Column( 

162 "description", UnicodeText, comment="Description of the photograph" 

163 ) 

164 photo_blobid = CamcopsColumn( 

165 "photo_blobid", 

166 Integer, 

167 is_blob_id_field=True, 

168 blob_relationship_attr_name="photo", 

169 comment="ID of the BLOB (foreign key to blobs.id, given " 

170 "matching device and current/frozen record status)", 

171 ) 

172 # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE: 

173 rotation = Column( # DEFUNCT as of v2.0.0 

174 "rotation", 

175 Integer, 

176 comment="(DEFUNCT COLUMN) " 

177 "Rotation (clockwise, in degrees) to be applied for viewing", 

178 ) 

179 

180 photo = blob_relationship("PhotoSequenceSinglePhoto", "photo_blobid") 

181 

182 def get_html_table_rows(self) -> str: 

183 # noinspection PyTypeChecker 

184 return """ 

185 <tr class="{CssClass.SUBHEADING}"> 

186 <td>Photo {num}: <b>{description}</b></td> 

187 </tr> 

188 <tr><td>{photo}</td></tr> 

189 """.format( 

190 CssClass=CssClass, 

191 num=self.seqnum, 

192 description=ws.webify(self.description), 

193 photo=get_blob_img_html(self.photo), 

194 ) 

195 

196 # ------------------------------------------------------------------------- 

197 # TaskDescendant overrides 

198 # ------------------------------------------------------------------------- 

199 

200 @classmethod 

201 def task_ancestor_class(cls) -> Optional[Type["Task"]]: 

202 return PhotoSequence 

203 

204 def task_ancestor(self) -> Optional["PhotoSequence"]: 

205 return PhotoSequence.get_linked(self.photosequence_id, self) 

206 

207 

208class PhotoSequence(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

209 """ 

210 Server implementation of the PhotoSequence task. 

211 """ 

212 

213 __tablename__ = "photosequence" 

214 shortname = "PhotoSequence" 

215 info_filename_stem = "clinical" 

216 

217 sequence_description = Column( 

218 "sequence_description", 

219 UnicodeText, 

220 comment="Description of the sequence of photographs", 

221 ) 

222 

223 photos = ancillary_relationship( 

224 parent_class_name="PhotoSequence", 

225 ancillary_class_name="PhotoSequenceSinglePhoto", 

226 ancillary_fk_to_parent_attr_name="photosequence_id", 

227 ancillary_order_by_attr_name="seqnum", 

228 ) # type: List[PhotoSequenceSinglePhoto] 

229 

230 @staticmethod 

231 def longname(req: "CamcopsRequest") -> str: 

232 _ = req.gettext 

233 return _("Photograph sequence") 

234 

235 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

236 infolist = [CtvInfo(content=self.sequence_description)] 

237 for p in self.photos: 

238 infolist.append(CtvInfo(content=p.description)) 

239 return infolist 

240 

241 def get_num_photos(self) -> int: 

242 return len(self.photos) 

243 

244 def is_complete(self) -> bool: 

245 # If you're wondering why this is being called unexpectedly: it may be 

246 # because this task is being displayed in the task list, at which point 

247 # we colour it by its complete-or-not status. 

248 return bool(self.sequence_description) and self.get_num_photos() > 0 

249 

250 def get_task_html(self, req: CamcopsRequest) -> str: 

251 html = f""" 

252 <div class="{CssClass.SUMMARY}"> 

253 <table class="{CssClass.SUMMARY}"> 

254 {self.get_is_complete_tr(req)} 

255 {tr_qa("Number of photos", self.get_num_photos())} 

256 {tr_qa("Description", self.sequence_description)} 

257 </table> 

258 </div> 

259 <table class="{CssClass.TASKDETAIL}"> 

260 """ 

261 for p in self.photos: 

262 html += p.get_html_table_rows() 

263 html += """ 

264 </table> 

265 """ 

266 return html 

267 

268 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

269 if not self.is_complete(): 

270 return [] 

271 return [ 

272 SnomedExpression(req.snomed(SnomedLookup.PHOTOGRAPH_PROCEDURE)), 

273 SnomedExpression( 

274 req.snomed(SnomedLookup.PHOTOGRAPH_PHYSICAL_OBJECT) 

275 ), 

276 ]