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

28 

29from typing import List, Optional, Type 

30 

31import cardinal_pythonlib.rnc_web as ws 

32from sqlalchemy.sql.schema import Column 

33from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

34 

35from camcops_server.cc_modules.cc_blob import ( 

36 Blob, 

37 blob_relationship, 

38 get_blob_img_html, 

39) 

40from camcops_server.cc_modules.cc_constants import CssClass 

41from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

42from camcops_server.cc_modules.cc_db import ( 

43 ancillary_relationship, 

44 GenericTabletRecordMixin, 

45 TaskDescendant, 

46) 

47from camcops_server.cc_modules.cc_html import answer, tr_qa 

48from camcops_server.cc_modules.cc_request import CamcopsRequest 

49from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

50from camcops_server.cc_modules.cc_sqla_coltypes import CamcopsColumn 

51from camcops_server.cc_modules.cc_sqlalchemy import Base 

52from camcops_server.cc_modules.cc_task import ( 

53 Task, 

54 TaskHasClinicianMixin, 

55 TaskHasPatientMixin, 

56) 

57 

58 

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

60# Photo 

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

62 

63class Photo(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

64 """ 

65 Server implementation of the Photo task. 

66 """ 

67 __tablename__ = "photo" 

68 shortname = "Photo" 

69 

70 description = Column( 

71 "description", UnicodeText, 

72 comment="Description of the photograph" 

73 ) 

74 photo_blobid = CamcopsColumn( 

75 "photo_blobid", Integer, 

76 is_blob_id_field=True, blob_relationship_attr_name="photo", 

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

78 "matching device and current/frozen record status)" 

79 ) 

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

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

82 "rotation", Integer, 

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

84 ) 

85 

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

87 

88 @staticmethod 

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

90 _ = req.gettext 

91 return _("Photograph") 

92 

93 def is_complete(self) -> bool: 

94 return self.photo_blobid is not None 

95 

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

97 if not self.is_complete(): 

98 return CTV_INCOMPLETE 

99 if not self.description: 

100 return [] 

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

102 

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

104 # noinspection PyTypeChecker 

105 return """ 

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

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

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

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

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

111 </table> 

112 """.format( 

113 CssClass=CssClass, 

114 description=answer( 

115 ws.webify(self.description), default="(No description)", 

116 default_for_blank_strings=True 

117 ), 

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

119 photo=get_blob_img_html(self.photo) 

120 ) 

121 

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

123 if not self.is_complete(): 

124 return [] 

125 return [ 

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

127 SnomedExpression(req.snomed(SnomedLookup.PHOTOGRAPH_PHYSICAL_OBJECT)), # noqa 

128 ] 

129 

130 

131# ============================================================================= 

132# PhotoSequence 

133# ============================================================================= 

134 

135class PhotoSequenceSinglePhoto(GenericTabletRecordMixin, TaskDescendant, Base): 

136 __tablename__ = "photosequence_photos" 

137 

138 photosequence_id = Column( 

139 "photosequence_id", Integer, nullable=False, 

140 comment="Tablet FK to photosequence" 

141 ) 

142 seqnum = Column( 

143 "seqnum", Integer, nullable=False, 

144 comment="Sequence number of this photo " 

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

146 ) 

147 description = Column( 

148 "description", UnicodeText, 

149 comment="Description of the photograph" 

150 ) 

151 photo_blobid = CamcopsColumn( 

152 "photo_blobid", Integer, 

153 is_blob_id_field=True, blob_relationship_attr_name="photo", 

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

155 "matching device and current/frozen record status)" 

156 ) 

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

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

159 "rotation", Integer, 

160 comment="(DEFUNCT COLUMN) " 

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

162 ) 

163 

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

165 

166 def get_html_table_rows(self) -> str: 

167 # noinspection PyTypeChecker 

168 return """ 

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

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

171 </tr> 

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

173 """.format( 

174 CssClass=CssClass, 

175 num=self.seqnum, 

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

177 photo=get_blob_img_html(self.photo) 

178 ) 

179 

180 # ------------------------------------------------------------------------- 

181 # TaskDescendant overrides 

182 # ------------------------------------------------------------------------- 

183 

184 @classmethod 

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

186 return PhotoSequence 

187 

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

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

190 

191 

192class PhotoSequence(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

193 """ 

194 Server implementation of the PhotoSequence task. 

195 """ 

196 __tablename__ = "photosequence" 

197 shortname = "PhotoSequence" 

198 

199 sequence_description = Column( 

200 "sequence_description", UnicodeText, 

201 comment="Description of the sequence of photographs" 

202 ) 

203 

204 photos = ancillary_relationship( 

205 parent_class_name="PhotoSequence", 

206 ancillary_class_name="PhotoSequenceSinglePhoto", 

207 ancillary_fk_to_parent_attr_name="photosequence_id", 

208 ancillary_order_by_attr_name="seqnum" 

209 ) # type: List[PhotoSequenceSinglePhoto] 

210 

211 @staticmethod 

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

213 _ = req.gettext 

214 return _("Photograph sequence") 

215 

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

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

218 for p in self.photos: 

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

220 return infolist 

221 

222 def get_num_photos(self) -> int: 

223 return len(self.photos) 

224 

225 def is_complete(self) -> bool: 

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

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

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

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

230 

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

232 html = f""" 

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

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

235 {self.get_is_complete_tr(req)} 

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

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

238 </table> 

239 </div> 

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

241 """ 

242 for p in self.photos: 

243 html += p.get_html_table_rows() 

244 html += """ 

245 </table> 

246 """ 

247 return html 

248 

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

250 if not self.is_complete(): 

251 return [] 

252 return [ 

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

254 SnomedExpression(req.snomed(SnomedLookup.PHOTOGRAPH_PHYSICAL_OBJECT)), # noqa 

255 ]