Coverage for cc_modules/cc_idnumdef.py: 77%
62 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_idnumdef.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**ID number definitions.**
28"""
30import logging
31from typing import List, Optional, Tuple, TYPE_CHECKING
33from cardinal_pythonlib.logs import BraceStyleAdapter
34from cardinal_pythonlib.nhs import is_valid_nhs_number
35from cardinal_pythonlib.reprfunc import simple_repr
36from sqlalchemy.orm import Mapped, mapped_column, Session as SqlASession
37from sqlalchemy.sql.sqltypes import String
39from camcops_server.cc_modules.cc_pyramid import Routes
40from camcops_server.cc_modules.cc_sqla_coltypes import (
41 HL7AssigningAuthorityType,
42 HL7IdTypeType,
43 IdDescriptorColType,
44 UrlColType,
45)
46from camcops_server.cc_modules.cc_sqlalchemy import Base
48if TYPE_CHECKING:
49 from camcops_server.cc_modules.cc_request import CamcopsRequest
51log = BraceStyleAdapter(logging.getLogger(__name__))
54# =============================================================================
55# ID number validation
56# =============================================================================
58ID_NUM_VALIDATION_METHOD_MAX_LEN = 50
61class IdNumValidationMethod(object):
62 """
63 Constants representing ways that CamCOPS knows to validate ID numbers.
64 """
66 NONE = "" # special
67 UK_NHS_NUMBER = "uk_nhs_number"
70ID_NUM_VALIDATION_METHOD_CHOICES = (
71 # for HTML forms: value, description
72 (IdNumValidationMethod.NONE, "None"),
73 (IdNumValidationMethod.UK_NHS_NUMBER, "UK NHS number"),
74)
77def validate_id_number(
78 req: "CamcopsRequest", idnum: Optional[int], method: str
79) -> Tuple[bool, str]:
80 """
81 Validates an ID number according to a method (as per
82 :class:`IdNumValidationMethod`).
84 If the number is ``None``, that's valid (that's an ID policy failure, not
85 a number validation failure). If ``method`` is falsy, that's also valid
86 (no constraints).
88 Args:
89 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
90 idnum: the ID number, or ``None``
91 method:
93 Returns:
94 tuple: ``valid, why_invalid`` where ``valid`` is ``bool`` and
95 ``why_invalid`` is ``str``.
97 """
98 _ = req.gettext
99 if idnum is None or not method:
100 return True, ""
101 if not isinstance(idnum, int):
102 return False, _("Not an integer")
103 if method == IdNumValidationMethod.UK_NHS_NUMBER:
104 if is_valid_nhs_number(idnum):
105 return True, ""
106 else:
107 return False, _("Invalid UK NHS number")
108 return False, _("Unknown validation method")
111# =============================================================================
112# IdNumDefinition
113# =============================================================================
116class IdNumDefinition(Base):
117 """
118 Represents an ID number definition.
119 """
121 __tablename__ = "_idnum_definitions"
123 which_idnum: Mapped[int] = mapped_column(
124 primary_key=True,
125 index=True,
126 comment="Which of the server's ID numbers is this?",
127 )
128 description: Mapped[Optional[str]] = mapped_column(
129 IdDescriptorColType,
130 comment="Full description of the ID number",
131 )
132 short_description: Mapped[Optional[str]] = mapped_column(
133 IdDescriptorColType,
134 comment="Short description of the ID number",
135 )
136 hl7_id_type: Mapped[Optional[str]] = mapped_column(
137 HL7IdTypeType,
138 comment="HL7: Identifier Type code: 'a code corresponding to the type "
139 "of identifier. In some cases, this code may be used as a "
140 'qualifier to the "Assigning Authority" component.\'',
141 )
142 hl7_assigning_authority: Mapped[Optional[str]] = mapped_column(
143 HL7AssigningAuthorityType,
144 comment="HL7: Assigning Authority for ID number (unique name of the "
145 "system/organization/agency/department that creates the data).",
146 )
147 validation_method: Mapped[Optional[str]] = mapped_column(
148 String(length=ID_NUM_VALIDATION_METHOD_MAX_LEN),
149 comment="Optional validation method",
150 )
151 fhir_id_system: Mapped[Optional[str]] = mapped_column(
152 "fhir_id_system", UrlColType, comment="FHIR external ID 'system' URL"
153 )
155 def __init__(
156 self,
157 which_idnum: int = None,
158 description: str = "",
159 short_description: str = "",
160 hl7_id_type: str = "",
161 hl7_assigning_authority: str = "",
162 validation_method: str = "",
163 fhir_id_system: str = "",
164 ):
165 # We permit a "blank" constructor for automatic copying, e.g. merge_db.
166 self.which_idnum = which_idnum
167 self.description = description
168 self.short_description = short_description
169 self.hl7_id_type = hl7_id_type
170 self.hl7_assigning_authority = hl7_assigning_authority
171 self.validation_method = validation_method
172 self.fhir_id_system = fhir_id_system
174 def __repr__(self) -> str:
175 return simple_repr(
176 self,
177 ["which_idnum", "description", "short_description"],
178 with_addr=False,
179 )
181 def _camcops_default_fhir_id_system(self, req: "CamcopsRequest") -> str:
182 """
183 The built-in FHIR ID system URL that we'll use if the user hasn't
184 specified one for the selected ID number type.
185 """
186 return req.route_url(
187 Routes.FHIR_PATIENT_ID_SYSTEM, which_idnum=self.which_idnum
188 ) # path will be e.g. /fhir_patient_id_system/3
190 def effective_fhir_id_system(self, req: "CamcopsRequest") -> str:
191 """
192 If the user has set a FHIR ID system, return that. Otherwise, return
193 a CamCOPS default.
194 """
195 return self.fhir_id_system or self._camcops_default_fhir_id_system(req)
197 def verbose_fhir_id_system(self, req: "CamcopsRequest") -> str:
198 """
199 Returns a human-readable description of the FHIR ID system in effect,
200 in HTML form.
201 """
202 _ = req.gettext
203 if self.fhir_id_system:
204 prefix = ""
205 url = self.fhir_id_system
206 else:
207 prefix = _("Default:") + " "
208 url = self._camcops_default_fhir_id_system(req)
209 return f'{prefix} <a href="{url}">{url}</a>'
212# =============================================================================
213# Retrieving all IdNumDefinition objects
214# =============================================================================
217def get_idnum_definitions(dbsession: SqlASession) -> List[IdNumDefinition]:
218 """
219 Get all ID number definitions from the database, in order.
220 """
221 return list(
222 dbsession.query(IdNumDefinition).order_by(IdNumDefinition.which_idnum)
223 )