Coverage for cc_modules/cc_simpleobjects.py: 58%
72 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_simpleobjects.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**Simple struct-like classes.**
28"""
30import copy
31from typing import Any, List, Optional, TYPE_CHECKING
33from pendulum import Date
35from cardinal_pythonlib.datetimefunc import format_datetime
36from cardinal_pythonlib.reprfunc import auto_repr
38from camcops_server.cc_modules.cc_constants import DateFormat
40if TYPE_CHECKING:
41 from camcops_server.cc_modules.cc_request import CamcopsRequest
43# Prefer classes to collections.namedtuple; both support type checking but
44# classes support better parameter checking (and refactoring) via PyCharm.
47# =============================================================================
48# IdNumReference
49# =============================================================================
52class IdNumReference(object):
53 """
54 A simple way of referring to an ID number.
56 It's not stored in the database -- it's just an object to be passed around
57 that encapsulates ``which_idnum`` and ``idnum_value``.
59 As an example, suppose our administrator has defined ID type
60 (``which_idnum``) 7 to be "NHS number". Then if a patient has NHS number
61 9999999999, we might represent this ID of theirs as
62 ``IdNumReference(which_idnum=7, idnum_value=9999999999)``.
63 """
65 def __init__(self, which_idnum: int, idnum_value: int) -> None:
66 self.which_idnum = which_idnum
67 self.idnum_value = idnum_value
69 def __str__(self) -> str:
70 return f"idnum{self.which_idnum}={self.idnum_value}"
72 def __repr__(self) -> str:
73 return auto_repr(self)
75 def is_valid(self) -> bool:
76 return (
77 self.which_idnum is not None
78 and self.which_idnum > 0
79 and self.idnum_value is not None
80 and self.idnum_value > 0
81 )
83 def __eq__(self, other: object) -> bool:
84 if not isinstance(other, IdNumReference):
85 return False
86 return (
87 self.which_idnum == other.which_idnum
88 and self.idnum_value == other.idnum_value
89 )
91 def description(self, req: "CamcopsRequest") -> str:
92 if not self.is_valid():
93 return "[invalid_IdNumReference]"
94 return f"{req.get_id_shortdesc(self.which_idnum)} = {self.idnum_value}"
97# =============================================================================
98# HL7PatientIdentifier
99# =============================================================================
102# noinspection PyShadowingBuiltins
103class HL7PatientIdentifier(object):
104 """
105 Represents a patient identifier for the HL7 protocol.
106 """
108 def __init__(
109 self, pid: str, id_type: str, assigning_authority: str
110 ) -> None:
111 self.pid = pid
112 # ... called "pid" not "id" as type checker sometimes thinks "id" must
113 # be integer, as in Python's id(object).
114 self.id_type = id_type
115 self.assigning_authority = assigning_authority
118# =============================================================================
119# BarePatientInfo
120# =============================================================================
123class BarePatientInfo(object):
124 """
125 Represents information about a patient using a simple object with no
126 connection to a database.
128 In some situations we avoid using
129 :class:`camcops_server.cc_modules.cc_patient.Patient`: specifically, when
130 we would otherwise have to deal with mutual dependency problems and the use
131 of the database (prior to full database initialization).
132 """
134 def __init__(
135 self,
136 forename: str = None,
137 surname: str = None,
138 sex: str = None,
139 dob: Optional[Date] = None,
140 address: str = None,
141 email: str = None,
142 gp: str = None,
143 otherdetails: str = None,
144 idnum_definitions: List[IdNumReference] = None,
145 ) -> None:
146 self.forename = forename
147 self.surname = surname
148 self.sex = sex
149 self.dob = dob
150 self.address = address
151 self.email = email
152 self.gp = gp
153 self.otherdetails = otherdetails
154 self.idnum_definitions = (
155 idnum_definitions or []
156 ) # type: List[IdNumReference]
158 def __str__(self) -> str:
159 return (
160 "Patient(forename={f!r}, surname={sur!r}, sex={sex!r}, DOB={dob}, "
161 "address={a!r}, email={email!r}, gp={gp!r}, otherdetails={o!r}, "
162 "idnums={i})".format(
163 f=self.forename,
164 sur=self.surname,
165 sex=self.sex,
166 dob=format_datetime(self.dob, DateFormat.ISO8601_DATE_ONLY),
167 a=self.address,
168 email=self.email,
169 gp=self.gp,
170 o=self.otherdetails,
171 i="[{}]".format(
172 ", ".join(str(idnum) for idnum in self.idnum_definitions)
173 ),
174 )
175 )
177 def __repr__(self) -> str:
178 return auto_repr(self)
180 def add_idnum(self, idref: IdNumReference) -> None:
181 """
182 Adds an ID number. No checks in relation to what's already present.
184 Args:
185 idref: a :class:`IdNumReference`
186 """
187 self.idnum_definitions.append(idref)
189 def __eq__(self, other: object) -> bool:
190 """
191 Do all data elements match those of ``other``?
192 """
193 if not isinstance(other, BarePatientInfo):
194 return False
195 return (
196 self.forename == other.forename
197 and self.surname == other.surname
198 and self.sex == other.sex
199 and self.dob == other.dob
200 and self.address == other.address
201 and self.email == other.email
202 and self.gp == other.gp
203 and self.otherdetails == other.otherdetails
204 and self.idnum_definitions == other.idnum_definitions
205 )
208# =============================================================================
209# Raw XML value
210# =============================================================================
213class XmlSimpleValue(object):
214 """
215 Represents XML lowest-level items. See functions in ``cc_xml.py``.
216 """
218 def __init__(self, value: Any) -> None:
219 self.value = value
222# =============================================================================
223# TaskExportOptions
224# =============================================================================
227class TaskExportOptions(object):
228 """
229 Information-holding object for options controlling XML and other
230 representations of tasks.
231 """
233 def __init__(
234 self,
235 db_patient_id_per_row: bool = False,
236 db_make_all_tables_even_empty: bool = False,
237 db_include_summaries: bool = False,
238 include_blobs: bool = False,
239 xml_include_ancillary: bool = False,
240 xml_include_calculated: bool = False,
241 xml_include_comments: bool = True,
242 xml_include_patient: bool = False,
243 xml_include_plain_columns: bool = False,
244 xml_include_snomed: bool = False,
245 xml_skip_fields: List[str] = None,
246 xml_sort_by_name: bool = True,
247 xml_with_header_comments: bool = False,
248 ) -> None:
249 """
250 Args:
251 db_patient_id_per_row:
252 generates an anonymisation staging database -- that is, a
253 database with patient IDs in every row of every table, suitable
254 for feeding into an anonymisation system like CRATE
255 (https://doi.org/10.1186%2Fs12911-017-0437-1).
256 db_make_all_tables_even_empty:
257 create all tables, even empty ones
259 include_blobs:
260 include binary large objects (BLOBs) (applies to several export
261 formats)
263 xml_include_ancillary:
264 include ancillary tables as well as the main?
265 xml_include_calculated:
266 include fields calculated by the task
267 xml_include_comments:
268 include comments in XML?
269 xml_include_patient:
270 include patient details?
271 xml_include_plain_columns:
272 include the base columns
273 xml_include_snomed:
274 include SNOMED-CT codes, if available?
275 xml_skip_fields:
276 fieldnames to skip
277 xml_sort_by_name:
278 sort by field/attribute names?
279 xml_with_header_comments:
280 include header-style comments?
281 """
282 self.db_patient_id_in_each_row = db_patient_id_per_row
283 self.db_make_all_tables_even_empty = db_make_all_tables_even_empty
284 self.db_include_summaries = db_include_summaries
286 self.include_blobs = include_blobs
288 self.xml_include_ancillary = xml_include_ancillary
289 self.xml_include_calculated = xml_include_calculated
290 self.xml_include_comments = xml_include_comments
291 self.xml_include_patient = xml_include_patient
292 self.xml_include_plain_columns = xml_include_plain_columns
293 self.xml_include_snomed = xml_include_snomed
294 self.xml_skip_fields = xml_skip_fields or [] # type: List[str]
295 self.xml_sort_by_name = xml_sort_by_name
296 self.xml_with_header_comments = xml_with_header_comments
298 def clone(self) -> "TaskExportOptions":
299 """
300 Returns a copy of this object.
301 """
302 return copy.copy(self)