Coverage for cc_modules/cc_patientidnum.py: 54%
79 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/cc_modules/cc_patientidnum.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**Represent patient ID numbers.**
30We were looking up ID descriptors from the device's stored variables.
31However, that is a bit of a nuisance for a server-side researcher, and
32it's a pain to copy the server's storedvar values (and -- all or some?)
33when a patient gets individually moved off the tablet. Anyway, they're
34important, so a little repetition is not the end of the world. So,
35let's have the tablet store its current ID descriptors in the patient
36record at the point of upload, and then it's available here directly.
37Thus, always complete and contemporaneous.
39... DECISION CHANGED 2017-07-08; see justification in tablet
40 overall_design.txt
42"""
44import logging
45from typing import List, Tuple, TYPE_CHECKING
47from cardinal_pythonlib.logs import BraceStyleAdapter
48from cardinal_pythonlib.reprfunc import simple_repr
49from sqlalchemy.orm import relationship
50from sqlalchemy.sql.schema import Column, ForeignKey
51from sqlalchemy.sql.sqltypes import BigInteger, Integer
53from camcops_server.cc_modules.cc_constants import (
54 EXTRA_COMMENT_PREFIX,
55 EXTRA_IDNUM_FIELD_PREFIX,
56 NUMBER_OF_IDNUMS_DEFUNCT,
57)
58from camcops_server.cc_modules.cc_db import GenericTabletRecordMixin
59from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition
60from camcops_server.cc_modules.cc_simpleobjects import IdNumReference
61from camcops_server.cc_modules.cc_sqla_coltypes import CamcopsColumn
62from camcops_server.cc_modules.cc_sqlalchemy import Base
64if TYPE_CHECKING:
65 from camcops_server.cc_modules.cc_patient import Patient
66 from camcops_server.cc_modules.cc_request import CamcopsRequest
68log = BraceStyleAdapter(logging.getLogger(__name__))
71# =============================================================================
72# PatientIdNum class
73# =============================================================================
74# Stores ID numbers for a specific patient
77class PatientIdNum(GenericTabletRecordMixin, Base):
78 """
79 SQLAlchemy ORM class representing an ID number (as a
80 which_idnum/idnum_value pair) for a patient.
81 """
83 __tablename__ = "patient_idnum"
85 id = Column(
86 "id",
87 Integer,
88 nullable=False,
89 comment="Primary key on the source tablet device",
90 )
91 patient_id = Column(
92 "patient_id",
93 Integer,
94 nullable=False,
95 comment="FK to patient.id (for this device/era)",
96 )
97 which_idnum = Column(
98 "which_idnum",
99 Integer,
100 ForeignKey(IdNumDefinition.which_idnum),
101 nullable=False,
102 comment="Which of the server's ID numbers is this?",
103 )
104 idnum_value = CamcopsColumn(
105 "idnum_value",
106 BigInteger,
107 identifies_patient=True,
108 comment="The value of the ID number",
109 )
110 # Note: we don't use a relationship() to IdNumDefinition here; we do that
111 # sort of work via the CamcopsRequest, which caches them for speed.
113 patient = relationship(
114 # https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#relationship-custom-foreign
115 # https://docs.sqlalchemy.org/en/latest/orm/relationship_api.html#sqlalchemy.orm.relationship # noqa
116 # https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#relationship-primaryjoin # noqa
117 "Patient",
118 primaryjoin=(
119 "and_("
120 " remote(Patient.id) == foreign(PatientIdNum.patient_id), "
121 " remote(Patient._device_id) == foreign(PatientIdNum._device_id), "
122 " remote(Patient._era) == foreign(PatientIdNum._era), "
123 " remote(Patient._current) == True "
124 ")"
125 ),
126 uselist=False,
127 viewonly=True,
128 )
130 # -------------------------------------------------------------------------
131 # String representations
132 # -------------------------------------------------------------------------
134 def __str__(self) -> str:
135 return f"idnum{self.which_idnum}={self.idnum_value}"
137 def prettystr(self, req: "CamcopsRequest") -> str:
138 """
139 A prettified version of __str__.
141 Args:
142 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
143 """
144 return f"{self.short_description(req)} {self.idnum_value}"
146 def full_prettystr(self, req: "CamcopsRequest") -> str:
147 """
148 A long-version prettified version of __str__.
150 Args:
151 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
152 """
153 return f"{self.description(req)} {self.idnum_value}"
155 def __repr__(self) -> str:
156 return simple_repr(
157 self,
158 [
159 "_pk",
160 "_device_id",
161 "_era",
162 "id",
163 "patient_id",
164 "which_idnum",
165 "idnum_value",
166 ],
167 )
169 # -------------------------------------------------------------------------
170 # Equality
171 # -------------------------------------------------------------------------
173 def __members(self) -> Tuple:
174 """
175 For :meth:`__hash__` and :meth:`__eq__`, as per
176 https://stackoverflow.com/questions/45164691/recommended-way-to-implement-eq-and-hash
177 """ # noqa
178 return self.which_idnum, self.idnum_value
180 def __hash__(self) -> int:
181 """
182 Must be compatible with __eq__.
184 See also
185 https://stackoverflow.com/questions/45164691/recommended-way-to-implement-eq-and-hash
186 """ # noqa
187 return hash(self.__members())
189 def __eq__(self, other: "PatientIdNum") -> bool:
190 """
191 Do ``self`` and ``other`` represent the same ID number?
193 Equivalent to:
195 .. code-block:: python
197 return (
198 self.which_idnum == other.which_idnum and
199 self.idnum_value == other.idnum_value and
200 self.which_idnum is not None and
201 self.idnum_value is not None
202 )
203 """
204 sm = self.__members()
205 return (
206 type(self) is type(other)
207 and (None not in sm)
208 and sm == other.__members()
209 )
211 # -------------------------------------------------------------------------
212 # Validity
213 # -------------------------------------------------------------------------
215 def is_superficially_valid(self) -> bool:
216 """
217 Is this a valid ID number?
218 """
219 return (
220 self.which_idnum is not None
221 and self.idnum_value is not None
222 and self.which_idnum >= 0
223 and self.idnum_value >= 0
224 )
226 def is_fully_valid(self, req: "CamcopsRequest") -> bool:
227 if not self.is_superficially_valid():
228 return False
229 return req.is_idnum_valid(self.which_idnum, self.idnum_value)
231 def why_invalid(self, req: "CamcopsRequest") -> str:
232 if not self.is_superficially_valid():
233 _ = req.gettext
234 return _("ID number fails basic checks")
235 return req.why_idnum_invalid(self.which_idnum, self.idnum_value)
237 # -------------------------------------------------------------------------
238 # ID type description
239 # -------------------------------------------------------------------------
241 def description(self, req: "CamcopsRequest") -> str:
242 """
243 Returns the full description for this ID number.
244 """
245 which_idnum = self.which_idnum # type: int
246 return req.get_id_desc(which_idnum, default="?")
248 def short_description(self, req: "CamcopsRequest") -> str:
249 """
250 Returns the short description for this ID number.
251 """
252 which_idnum = self.which_idnum # type: int
253 return req.get_id_shortdesc(which_idnum, default="?")
255 # -------------------------------------------------------------------------
256 # Other representations
257 # -------------------------------------------------------------------------
259 def get_idnum_reference(self) -> IdNumReference:
260 """
261 Returns an
262 :class:`camcops_server.cc_modules.cc_simpleobjects.IdNumReference`
263 object summarizing this ID number.
264 """
265 return IdNumReference(
266 which_idnum=self.which_idnum, idnum_value=self.idnum_value
267 )
269 def get_filename_component(self, req: "CamcopsRequest") -> str:
270 """
271 Returns a string including the short description of the ID number, and
272 the number itself, for use in filenames.
273 """
274 if self.which_idnum is None or self.idnum_value is None:
275 return ""
276 return f"{self.short_description(req)}-{self.idnum_value}"
278 # -------------------------------------------------------------------------
279 # Set value
280 # -------------------------------------------------------------------------
282 def set_idnum(self, idnum_value: int) -> None:
283 """
284 Sets the ID number value.
285 """
286 self.idnum_value = idnum_value
288 # -------------------------------------------------------------------------
289 # Patient
290 # -------------------------------------------------------------------------
292 def get_patient_server_pk(self) -> int:
293 patient = self.patient # type: Patient
294 if not patient:
295 raise ValueError(
296 "Corrupted database? PatientIdNum can't fetch its Patient"
297 )
298 return patient.pk
301# =============================================================================
302# Fake ID values when upgrading from old ID number system
303# =============================================================================
306def fake_tablet_id_for_patientidnum(patient_id: int, which_idnum: int) -> int:
307 """
308 Returns a fake client-side PK (tablet ID) for a patient number. Only for
309 use in upgrading old databases.
310 """
311 return patient_id * NUMBER_OF_IDNUMS_DEFUNCT + which_idnum
314# =============================================================================
315# Additional ID number column info for DB_PATIENT_ID_PER_ROW export option
316# =============================================================================
319def extra_id_colname(which_idnum: int) -> str:
320 """
321 The column name used for the extra ID number columns provided by the
322 ``DB_PATIENT_ID_PER_ROW`` export option.
324 Args:
325 which_idnum: ID number type
327 Returns:
328 str: ``idnum<which_idnum>``
330 """
331 return f"{EXTRA_IDNUM_FIELD_PREFIX}{which_idnum}"
334def extra_id_column(req: "CamcopsRequest", which_idnum: int) -> CamcopsColumn:
335 """
336 The column definition used for the extra ID number columns provided by the
337 ``DB_PATIENT_ID_PER_ROW`` export option.
339 Args:
340 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
341 which_idnum: ID number type
343 Returns:
344 the column definition
346 """
347 desc = req.get_id_desc(which_idnum)
348 return CamcopsColumn(
349 extra_id_colname(which_idnum),
350 BigInteger,
351 identifies_patient=True,
352 comment=EXTRA_COMMENT_PREFIX + f"ID number {which_idnum}: {desc}",
353 )
356def all_extra_id_columns(req: "CamcopsRequest") -> List[CamcopsColumn]:
357 """
358 Returns all column definitions used for the extra ID number columns
359 provided by the ``DB_PATIENT_ID_PER_ROW`` export option.
361 Args:
362 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
364 Returns:
365 list: the column definitions
366 """
367 return [
368 extra_id_column(req, which_idnum)
369 for which_idnum in req.valid_which_idnums
370 ]