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

1""" 

2camcops_server/cc_modules/cc_idnumdef.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

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. 

15 

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. 

20 

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/>. 

23 

24=============================================================================== 

25 

26**ID number definitions.** 

27 

28""" 

29 

30import logging 

31from typing import List, Optional, Tuple, TYPE_CHECKING 

32 

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 

38 

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 

47 

48if TYPE_CHECKING: 

49 from camcops_server.cc_modules.cc_request import CamcopsRequest 

50 

51log = BraceStyleAdapter(logging.getLogger(__name__)) 

52 

53 

54# ============================================================================= 

55# ID number validation 

56# ============================================================================= 

57 

58ID_NUM_VALIDATION_METHOD_MAX_LEN = 50 

59 

60 

61class IdNumValidationMethod(object): 

62 """ 

63 Constants representing ways that CamCOPS knows to validate ID numbers. 

64 """ 

65 

66 NONE = "" # special 

67 UK_NHS_NUMBER = "uk_nhs_number" 

68 

69 

70ID_NUM_VALIDATION_METHOD_CHOICES = ( 

71 # for HTML forms: value, description 

72 (IdNumValidationMethod.NONE, "None"), 

73 (IdNumValidationMethod.UK_NHS_NUMBER, "UK NHS number"), 

74) 

75 

76 

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`). 

83 

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). 

87 

88 Args: 

89 req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` 

90 idnum: the ID number, or ``None`` 

91 method: 

92 

93 Returns: 

94 tuple: ``valid, why_invalid`` where ``valid`` is ``bool`` and 

95 ``why_invalid`` is ``str``. 

96 

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") 

109 

110 

111# ============================================================================= 

112# IdNumDefinition 

113# ============================================================================= 

114 

115 

116class IdNumDefinition(Base): 

117 """ 

118 Represents an ID number definition. 

119 """ 

120 

121 __tablename__ = "_idnum_definitions" 

122 

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 ) 

154 

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 

173 

174 def __repr__(self) -> str: 

175 return simple_repr( 

176 self, 

177 ["which_idnum", "description", "short_description"], 

178 with_addr=False, 

179 ) 

180 

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 

189 

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) 

196 

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>' 

210 

211 

212# ============================================================================= 

213# Retrieving all IdNumDefinition objects 

214# ============================================================================= 

215 

216 

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 )