Coverage for cc_modules/cc_patientidnum.py: 57%
81 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 15:51 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 15:51 +0100
1"""
2camcops_server/cc_modules/cc_patientidnum.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CamCOPS is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26**Represent patient ID numbers.**
28We were looking up ID descriptors from the device's stored variables.
29However, that is a bit of a nuisance for a server-side researcher, and
30it's a pain to copy the server's storedvar values (and -- all or some?)
31when a patient gets individually moved off the tablet. Anyway, they're
32important, so a little repetition is not the end of the world. So,
33let's have the tablet store its current ID descriptors in the patient
34record at the point of upload, and then it's available here directly.
35Thus, always complete and contemporaneous.
37... DECISION CHANGED 2017-07-08; see justification in tablet
38 overall_design.txt
40"""
42import logging
43from typing import List, Optional, Tuple, TYPE_CHECKING
45from cardinal_pythonlib.logs import BraceStyleAdapter
46from cardinal_pythonlib.reprfunc import simple_repr
47from sqlalchemy.orm import mapped_column, Mapped, relationship
48from sqlalchemy.sql.schema import Column, ForeignKey
49from sqlalchemy.sql.sqltypes import BigInteger
51from camcops_server.cc_modules.cc_constants import (
52 EXTRA_COMMENT_PREFIX,
53 EXTRA_IDNUM_FIELD_PREFIX,
54 NUMBER_OF_IDNUMS_DEFUNCT,
55)
56from camcops_server.cc_modules.cc_db import GenericTabletRecordMixin
57from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition
58from camcops_server.cc_modules.cc_simpleobjects import IdNumReference
59from camcops_server.cc_modules.cc_sqla_coltypes import (
60 mapped_camcops_column,
61 camcops_column,
62)
63from camcops_server.cc_modules.cc_sqlalchemy import Base
65if TYPE_CHECKING:
66 from camcops_server.cc_modules.cc_patient import Patient
67 from camcops_server.cc_modules.cc_request import CamcopsRequest
69log = BraceStyleAdapter(logging.getLogger(__name__))
72# =============================================================================
73# PatientIdNum class
74# =============================================================================
75# Stores ID numbers for a specific patient
78class PatientIdNum(GenericTabletRecordMixin, Base):
79 """
80 SQLAlchemy ORM class representing an ID number (as a
81 which_idnum/idnum_value pair) for a patient.
82 """
84 __tablename__ = "patient_idnum"
86 id: Mapped[int] = mapped_column(
87 comment="Primary key on the source tablet device",
88 )
89 patient_id: Mapped[int] = mapped_column(
90 comment="FK to patient.id (for this device/era)",
91 )
92 which_idnum: Mapped[int] = mapped_column(
93 ForeignKey(IdNumDefinition.which_idnum),
94 comment="Which of the server's ID numbers is this?",
95 )
96 idnum_value: Mapped[Optional[int]] = mapped_camcops_column(
97 "idnum_value",
98 BigInteger,
99 identifies_patient=True,
100 comment="The value of the ID number",
101 )
102 # Note: we don't use a relationship() to IdNumDefinition here; we do that
103 # sort of work via the CamcopsRequest, which caches them for speed.
105 patient = relationship(
106 # https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#relationship-custom-foreign
107 # https://docs.sqlalchemy.org/en/latest/orm/relationship_api.html#sqlalchemy.orm.relationship # noqa
108 # https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#relationship-primaryjoin # noqa
109 "Patient",
110 primaryjoin=(
111 "and_("
112 " remote(Patient.id) == foreign(PatientIdNum.patient_id), "
113 " remote(Patient._device_id) == foreign(PatientIdNum._device_id), "
114 " remote(Patient._era) == foreign(PatientIdNum._era), "
115 " remote(Patient._current) == True "
116 ")"
117 ),
118 uselist=False,
119 viewonly=True,
120 )
122 duplicates: Mapped[list["PatientIdNum"]] = relationship(
123 primaryjoin=(
124 "and_("
125 " remote(PatientIdNum._pk) != foreign(PatientIdNum._pk), "
126 " remote(PatientIdNum._group_id) == foreign(PatientIdNum._group_id), " # noqa: E501
127 " remote(PatientIdNum.which_idnum) == foreign(PatientIdNum.which_idnum), " # noqa: E501
128 " remote(PatientIdNum.idnum_value) == foreign(PatientIdNum.idnum_value), " # noqa: E501
129 " remote(PatientIdNum._device_id) == foreign(PatientIdNum._device_id), " # noqa: E501
130 " remote(PatientIdNum._era) == foreign(PatientIdNum._era), "
131 " remote(PatientIdNum._current) == True, "
132 ")"
133 ),
134 viewonly=True,
135 uselist=True,
136 )
138 # -------------------------------------------------------------------------
139 # String representations
140 # -------------------------------------------------------------------------
142 def __str__(self) -> str:
143 return f"idnum{self.which_idnum}={self.idnum_value}"
145 def prettystr(self, req: "CamcopsRequest") -> str:
146 """
147 A prettified version of __str__.
149 Args:
150 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
151 """
152 return f"{self.short_description(req)} {self.idnum_value}"
154 def full_prettystr(self, req: "CamcopsRequest") -> str:
155 """
156 A long-version prettified version of __str__.
158 Args:
159 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
160 """
161 return f"{self.description(req)} {self.idnum_value}"
163 def __repr__(self) -> str:
164 return simple_repr(
165 self,
166 [
167 "_pk",
168 "_device_id",
169 "_era",
170 "id",
171 "patient_id",
172 "which_idnum",
173 "idnum_value",
174 ],
175 )
177 # -------------------------------------------------------------------------
178 # Equality
179 # -------------------------------------------------------------------------
181 def __members(self) -> Tuple:
182 """
183 For :meth:`__hash__` and :meth:`__eq__`, as per
184 https://stackoverflow.com/questions/45164691/recommended-way-to-implement-eq-and-hash
185 """
186 return self.which_idnum, self.idnum_value
188 def __hash__(self) -> int:
189 """
190 Must be compatible with __eq__.
192 See also
193 https://stackoverflow.com/questions/45164691/recommended-way-to-implement-eq-and-hash
194 """
195 return hash(self.__members())
197 def __eq__(self, other: object) -> bool:
198 """
199 Do ``self`` and ``other`` represent the same ID number?
201 Equivalent to:
203 .. code-block:: python
205 return (
206 self.which_idnum == other.which_idnum and
207 self.idnum_value == other.idnum_value and
208 self.which_idnum is not None and
209 self.idnum_value is not None
210 )
211 """
212 if not isinstance(other, PatientIdNum):
213 return NotImplemented
215 sm = self.__members()
216 return (None not in sm) and sm == other.__members()
218 # -------------------------------------------------------------------------
219 # Validity
220 # -------------------------------------------------------------------------
222 def is_superficially_valid(self) -> bool:
223 """
224 Is this a valid ID number?
225 """
226 return (
227 self.which_idnum is not None
228 and self.idnum_value is not None
229 and self.which_idnum >= 0
230 and self.idnum_value >= 0
231 )
233 def is_fully_valid(self, req: "CamcopsRequest") -> bool:
234 if not self.is_superficially_valid():
235 return False
236 return req.is_idnum_valid(self.which_idnum, self.idnum_value)
238 def why_invalid(self, req: "CamcopsRequest") -> str:
239 if not self.is_superficially_valid():
240 _ = req.gettext
241 return _("ID number fails basic checks")
242 return req.why_idnum_invalid(self.which_idnum, self.idnum_value)
244 # -------------------------------------------------------------------------
245 # ID type description
246 # -------------------------------------------------------------------------
248 def description(self, req: "CamcopsRequest") -> str:
249 """
250 Returns the full description for this ID number.
251 """
252 which_idnum = self.which_idnum # type: int
253 return req.get_id_desc(which_idnum, default="?")
255 def short_description(self, req: "CamcopsRequest") -> str:
256 """
257 Returns the short description for this ID number.
258 """
259 which_idnum = self.which_idnum # type: int
260 return req.get_id_shortdesc(which_idnum, default="?")
262 # -------------------------------------------------------------------------
263 # Other representations
264 # -------------------------------------------------------------------------
266 def get_idnum_reference(self) -> IdNumReference:
267 """
268 Returns an
269 :class:`camcops_server.cc_modules.cc_simpleobjects.IdNumReference`
270 object summarizing this ID number.
271 """
272 return IdNumReference(
273 which_idnum=self.which_idnum, idnum_value=self.idnum_value
274 )
276 def get_filename_component(self, req: "CamcopsRequest") -> str:
277 """
278 Returns a string including the short description of the ID number, and
279 the number itself, for use in filenames.
280 """
281 if self.which_idnum is None or self.idnum_value is None:
282 return ""
283 return f"{self.short_description(req)}-{self.idnum_value}"
285 # -------------------------------------------------------------------------
286 # Set value
287 # -------------------------------------------------------------------------
289 def set_idnum(self, idnum_value: int) -> None:
290 """
291 Sets the ID number value.
292 """
293 self.idnum_value = idnum_value
295 # -------------------------------------------------------------------------
296 # Patient
297 # -------------------------------------------------------------------------
299 def get_patient_server_pk(self) -> int:
300 patient = self.patient # type: Patient
301 if not patient:
302 raise ValueError(
303 "Corrupted database? PatientIdNum can't fetch its Patient"
304 )
305 return patient.pk
308# =============================================================================
309# Fake ID values when upgrading from old ID number system
310# =============================================================================
313def fake_tablet_id_for_patientidnum(patient_id: int, which_idnum: int) -> int:
314 """
315 Returns a fake client-side PK (tablet ID) for a patient number. Only for
316 use in upgrading old databases.
317 """
318 return patient_id * NUMBER_OF_IDNUMS_DEFUNCT + which_idnum
321# =============================================================================
322# Additional ID number column info for DB_PATIENT_ID_PER_ROW export option
323# =============================================================================
326def extra_id_colname(which_idnum: int) -> str:
327 """
328 The column name used for the extra ID number columns provided by the
329 ``DB_PATIENT_ID_PER_ROW`` export option.
331 Args:
332 which_idnum: ID number type
334 Returns:
335 str: ``idnum<which_idnum>``
337 """
338 return f"{EXTRA_IDNUM_FIELD_PREFIX}{which_idnum}"
341def extra_id_column(req: "CamcopsRequest", which_idnum: int) -> Column:
342 """
343 The column definition used for the extra ID number columns provided by the
344 ``DB_PATIENT_ID_PER_ROW`` export option.
346 Args:
347 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
348 which_idnum: ID number type
350 Returns:
351 the column definition
353 """
354 desc = req.get_id_desc(which_idnum)
355 return camcops_column(
356 extra_id_colname(which_idnum),
357 BigInteger,
358 identifies_patient=True,
359 comment=EXTRA_COMMENT_PREFIX + f"ID number {which_idnum}: {desc}",
360 )
363def all_extra_id_columns(req: "CamcopsRequest") -> List[Column]:
364 """
365 Returns all column definitions used for the extra ID number columns
366 provided by the ``DB_PATIENT_ID_PER_ROW`` export option.
368 Args:
369 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
371 Returns:
372 list: the column definitions
373 """
374 return [
375 extra_id_column(req, which_idnum)
376 for which_idnum in req.valid_which_idnums
377 ]