Coverage for cc_modules/cc_patientidnum.py : 54%

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/cc_modules/cc_patientidnum.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**Represent patient ID numbers.**
29We were looking up ID descriptors from the device's stored variables.
30However, that is a bit of a nuisance for a server-side researcher, and
31it's a pain to copy the server's storedvar values (and -- all or some?)
32when a patient gets individually moved off the tablet. Anyway, they're
33important, so a little repetition is not the end of the world. So,
34let's have the tablet store its current ID descriptors in the patient
35record at the point of upload, and then it's available here directly.
36Thus, always complete and contemporaneous.
38... DECISION CHANGED 2017-07-08; see justification in tablet
39 overall_design.txt
41"""
43import logging
44from typing import List, Tuple, TYPE_CHECKING
46from cardinal_pythonlib.logs import BraceStyleAdapter
47from cardinal_pythonlib.reprfunc import simple_repr
48from sqlalchemy.orm import relationship
49from sqlalchemy.sql.schema import Column, ForeignKey
50from sqlalchemy.sql.sqltypes import BigInteger, Integer
52from camcops_server.cc_modules.cc_constants import (
53 EXTRA_COMMENT_PREFIX,
54 EXTRA_IDNUM_FIELD_PREFIX,
55 NUMBER_OF_IDNUMS_DEFUNCT,
56)
57from camcops_server.cc_modules.cc_db import GenericTabletRecordMixin
58from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition
59from camcops_server.cc_modules.cc_simpleobjects import IdNumReference
60from camcops_server.cc_modules.cc_sqla_coltypes import CamcopsColumn
61from camcops_server.cc_modules.cc_sqlalchemy import Base
63if TYPE_CHECKING:
64 from camcops_server.cc_modules.cc_patient import Patient
65 from camcops_server.cc_modules.cc_request import CamcopsRequest
67log = BraceStyleAdapter(logging.getLogger(__name__))
70# =============================================================================
71# PatientIdNum class
72# =============================================================================
73# Stores ID numbers for a specific patient
75class PatientIdNum(GenericTabletRecordMixin, Base):
76 """
77 SQLAlchemy ORM class representing an ID number (as a
78 which_idnum/idnum_value pair) for a patient.
79 """
80 __tablename__ = "patient_idnum"
82 id = Column(
83 "id", Integer,
84 nullable=False,
85 comment="Primary key on the source tablet device"
86 )
87 patient_id = Column(
88 "patient_id", Integer,
89 nullable=False,
90 comment="FK to patient.id (for this device/era)"
91 )
92 which_idnum = Column(
93 "which_idnum", Integer, ForeignKey(IdNumDefinition.which_idnum),
94 nullable=False,
95 comment="Which of the server's ID numbers is this?"
96 )
97 idnum_value = CamcopsColumn(
98 "idnum_value", 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 # http://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#relationship-custom-foreign
107 # http://docs.sqlalchemy.org/en/latest/orm/relationship_api.html#sqlalchemy.orm.relationship # noqa
108 # http://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 # -------------------------------------------------------------------------
123 # String representations
124 # -------------------------------------------------------------------------
126 def __str__(self) -> str:
127 return f"idnum{self.which_idnum}={self.idnum_value}"
129 def prettystr(self, req: "CamcopsRequest") -> str:
130 """
131 A prettified version of __str__.
133 Args:
134 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
135 """
136 return f"{self.short_description(req)} {self.idnum_value}"
138 def full_prettystr(self, req: "CamcopsRequest") -> str:
139 """
140 A long-version prettified version of __str__.
142 Args:
143 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
144 """
145 return f"{self.description(req)} {self.idnum_value}"
147 def __repr__(self) -> str:
148 return simple_repr(self, [
149 "_pk", "_device_id", "_era",
150 "id", "patient_id", "which_idnum", "idnum_value"
151 ])
153 # -------------------------------------------------------------------------
154 # Equality
155 # -------------------------------------------------------------------------
157 def __members(self) -> Tuple:
158 """
159 For :meth:`__hash__` and :meth:`__eq__`, as per
160 https://stackoverflow.com/questions/45164691/recommended-way-to-implement-eq-and-hash
161 """ # noqa
162 return self.which_idnum, self.idnum_value
164 def __hash__(self) -> int:
165 """
166 Must be compatible with __eq__.
168 See also
169 https://stackoverflow.com/questions/45164691/recommended-way-to-implement-eq-and-hash
170 """ # noqa
171 return hash(self.__members())
173 def __eq__(self, other: "PatientIdNum") -> bool:
174 """
175 Do ``self`` and ``other`` represent the same ID number?
177 Equivalent to:
179 .. code-block:: python
181 return (
182 self.which_idnum == other.which_idnum and
183 self.idnum_value == other.idnum_value and
184 self.which_idnum is not None and
185 self.idnum_value is not None
186 )
187 """
188 sm = self.__members()
189 return (
190 type(self) is type(other) and
191 (None not in sm) and
192 sm == other.__members()
193 )
195 # -------------------------------------------------------------------------
196 # Validity
197 # -------------------------------------------------------------------------
199 def is_superficially_valid(self) -> bool:
200 """
201 Is this a valid ID number?
202 """
203 return (
204 self.which_idnum is not None and
205 self.idnum_value is not None and
206 self.which_idnum >= 0 and
207 self.idnum_value >= 0
208 )
210 def is_fully_valid(self, req: "CamcopsRequest") -> bool:
211 if not self.is_superficially_valid():
212 return False
213 return req.is_idnum_valid(self.which_idnum, self.idnum_value)
215 def why_invalid(self, req: "CamcopsRequest") -> str:
216 if not self.is_superficially_valid():
217 _ = req.gettext
218 return _("ID number fails basic checks")
219 return req.why_idnum_invalid(self.which_idnum, self.idnum_value)
221 # -------------------------------------------------------------------------
222 # ID type description
223 # -------------------------------------------------------------------------
225 def description(self, req: "CamcopsRequest") -> str:
226 """
227 Returns the full description for this ID number.
228 """
229 which_idnum = self.which_idnum # type: int
230 return req.get_id_desc(which_idnum, default="?")
232 def short_description(self, req: "CamcopsRequest") -> str:
233 """
234 Returns the short description for this ID number.
235 """
236 which_idnum = self.which_idnum # type: int
237 return req.get_id_shortdesc(which_idnum, default="?")
239 # -------------------------------------------------------------------------
240 # Other representations
241 # -------------------------------------------------------------------------
243 def get_idnum_reference(self) -> IdNumReference:
244 """
245 Returns an
246 :class:`camcops_server.cc_modules.cc_simpleobjects.IdNumReference`
247 object summarizing this ID number.
248 """
249 return IdNumReference(which_idnum=self.which_idnum,
250 idnum_value=self.idnum_value)
252 def get_filename_component(self, req: "CamcopsRequest") -> str:
253 """
254 Returns a string including the short description of the ID number, and
255 the number itself, for use in filenames.
256 """
257 if self.which_idnum is None or self.idnum_value is None:
258 return ""
259 return f"{self.short_description(req)}-{self.idnum_value}"
261 # -------------------------------------------------------------------------
262 # Set value
263 # -------------------------------------------------------------------------
265 def set_idnum(self, idnum_value: int) -> None:
266 """
267 Sets the ID number value.
268 """
269 self.idnum_value = idnum_value
271 # -------------------------------------------------------------------------
272 # Patient
273 # -------------------------------------------------------------------------
275 def get_patient_server_pk(self) -> int:
276 patient = self.patient # type: Patient
277 if not patient:
278 raise ValueError(
279 "Corrupted database? PatientIdNum can't fetch its Patient")
280 return patient.pk
283# =============================================================================
284# Fake ID values when upgrading from old ID number system
285# =============================================================================
287def fake_tablet_id_for_patientidnum(patient_id: int, which_idnum: int) -> int:
288 """
289 Returns a fake client-side PK (tablet ID) for a patient number. Only for
290 use in upgrading old databases.
291 """
292 return patient_id * NUMBER_OF_IDNUMS_DEFUNCT + which_idnum
295# =============================================================================
296# Additional ID number column info for DB_PATIENT_ID_PER_ROW export option
297# =============================================================================
299def extra_id_colname(which_idnum: int) -> str:
300 """
301 The column name used for the extra ID number columns provided by the
302 ``DB_PATIENT_ID_PER_ROW`` export option.
304 Args:
305 which_idnum: ID number type
307 Returns:
308 str: ``idnum<which_idnum>``
310 """
311 return f"{EXTRA_IDNUM_FIELD_PREFIX}{which_idnum}"
314def extra_id_column(req: "CamcopsRequest", which_idnum: int) -> CamcopsColumn:
315 """
316 The column definition used for the extra ID number columns provided by the
317 ``DB_PATIENT_ID_PER_ROW`` export option.
319 Args:
320 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
321 which_idnum: ID number type
323 Returns:
324 the column definition
326 """
327 desc = req.get_id_desc(which_idnum)
328 return CamcopsColumn(
329 extra_id_colname(which_idnum),
330 BigInteger,
331 identifies_patient=True,
332 comment=EXTRA_COMMENT_PREFIX + f"ID number {which_idnum}: {desc}"
333 )
336def all_extra_id_columns(req: "CamcopsRequest") -> List[CamcopsColumn]:
337 """
338 Returns all column definitions used for the extra ID number columns
339 provided by the ``DB_PATIENT_ID_PER_ROW`` export option.
341 Args:
342 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
344 Returns:
345 list: the column definitions
346 """
347 return [
348 extra_id_column(req, which_idnum)
349 for which_idnum in req.valid_which_idnums
350 ]