Coverage for tasks/photo.py: 64%
85 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/photo.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28"""
30from typing import List, Optional, Type
32import cardinal_pythonlib.rnc_web as ws
33from sqlalchemy.sql.schema import Column
34from sqlalchemy.sql.sqltypes import Integer, UnicodeText
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)
60# =============================================================================
61# Photo
62# =============================================================================
65class Photo(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
66 """
67 Server implementation of the Photo task.
68 """
70 __tablename__ = "photo"
71 shortname = "Photo"
72 info_filename_stem = "clinical"
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 )
92 photo = blob_relationship("Photo", "photo_blobid") # type: Optional[Blob]
94 @staticmethod
95 def longname(req: "CamcopsRequest") -> str:
96 _ = req.gettext
97 return _("Photograph")
99 def is_complete(self) -> bool:
100 return self.photo_blobid is not None
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)]
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 )
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 ]
140# =============================================================================
141# PhotoSequence
142# =============================================================================
145class PhotoSequenceSinglePhoto(GenericTabletRecordMixin, TaskDescendant, Base):
146 __tablename__ = "photosequence_photos"
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 )
180 photo = blob_relationship("PhotoSequenceSinglePhoto", "photo_blobid")
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 )
196 # -------------------------------------------------------------------------
197 # TaskDescendant overrides
198 # -------------------------------------------------------------------------
200 @classmethod
201 def task_ancestor_class(cls) -> Optional[Type["Task"]]:
202 return PhotoSequence
204 def task_ancestor(self) -> Optional["PhotoSequence"]:
205 return PhotoSequence.get_linked(self.photosequence_id, self)
208class PhotoSequence(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
209 """
210 Server implementation of the PhotoSequence task.
211 """
213 __tablename__ = "photosequence"
214 shortname = "PhotoSequence"
215 info_filename_stem = "clinical"
217 sequence_description = Column(
218 "sequence_description",
219 UnicodeText,
220 comment="Description of the sequence of photographs",
221 )
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]
230 @staticmethod
231 def longname(req: "CamcopsRequest") -> str:
232 _ = req.gettext
233 return _("Photograph sequence")
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
241 def get_num_photos(self) -> int:
242 return len(self.photos)
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
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
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 ]