Coverage for tasks/photo.py : 63%

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
3"""
4camcops_server/tasks/photo.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
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.
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.
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/>.
25===============================================================================
27"""
29from typing import List, Optional, Type
31import cardinal_pythonlib.rnc_web as ws
32from sqlalchemy.sql.schema import Column
33from sqlalchemy.sql.sqltypes import Integer, UnicodeText
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)
59# =============================================================================
60# Photo
61# =============================================================================
63class Photo(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
64 """
65 Server implementation of the Photo task.
66 """
67 __tablename__ = "photo"
68 shortname = "Photo"
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 )
86 photo = blob_relationship("Photo", "photo_blobid") # type: Optional[Blob]
88 @staticmethod
89 def longname(req: "CamcopsRequest") -> str:
90 _ = req.gettext
91 return _("Photograph")
93 def is_complete(self) -> bool:
94 return self.photo_blobid is not None
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)]
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 )
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 ]
131# =============================================================================
132# PhotoSequence
133# =============================================================================
135class PhotoSequenceSinglePhoto(GenericTabletRecordMixin, TaskDescendant, Base):
136 __tablename__ = "photosequence_photos"
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 )
164 photo = blob_relationship("PhotoSequenceSinglePhoto", "photo_blobid")
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 )
180 # -------------------------------------------------------------------------
181 # TaskDescendant overrides
182 # -------------------------------------------------------------------------
184 @classmethod
185 def task_ancestor_class(cls) -> Optional[Type["Task"]]:
186 return PhotoSequence
188 def task_ancestor(self) -> Optional["PhotoSequence"]:
189 return PhotoSequence.get_linked(self.photosequence_id, self)
192class PhotoSequence(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
193 """
194 Server implementation of the PhotoSequence task.
195 """
196 __tablename__ = "photosequence"
197 shortname = "PhotoSequence"
199 sequence_description = Column(
200 "sequence_description", UnicodeText,
201 comment="Description of the sequence of photographs"
202 )
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]
211 @staticmethod
212 def longname(req: "CamcopsRequest") -> str:
213 _ = req.gettext
214 return _("Photograph sequence")
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
222 def get_num_photos(self) -> int:
223 return len(self.photos)
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
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
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 ]