Coverage for cc_modules/cc_idnumdef.py: 66%

64 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_idnumdef.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28**ID number definitions.** 

29 

30""" 

31 

32import logging 

33from typing import List, Optional, Tuple, TYPE_CHECKING 

34 

35from cardinal_pythonlib.logs import BraceStyleAdapter 

36from cardinal_pythonlib.nhs import is_valid_nhs_number 

37from cardinal_pythonlib.reprfunc import simple_repr 

38from sqlalchemy.orm import Session as SqlASession 

39from sqlalchemy.sql.schema import Column 

40from sqlalchemy.sql.sqltypes import Integer, String 

41 

42from camcops_server.cc_modules.cc_pyramid import Routes 

43from camcops_server.cc_modules.cc_sqla_coltypes import ( 

44 HL7AssigningAuthorityType, 

45 HL7IdTypeType, 

46 IdDescriptorColType, 

47 UrlColType, 

48) 

49from camcops_server.cc_modules.cc_sqlalchemy import Base 

50 

51if TYPE_CHECKING: 

52 from camcops_server.cc_modules.cc_request import CamcopsRequest 

53 

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

55 

56 

57# ============================================================================= 

58# ID number validation 

59# ============================================================================= 

60 

61ID_NUM_VALIDATION_METHOD_MAX_LEN = 50 

62 

63 

64class IdNumValidationMethod(object): 

65 """ 

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

67 """ 

68 

69 NONE = "" # special 

70 UK_NHS_NUMBER = "uk_nhs_number" 

71 

72 

73ID_NUM_VALIDATION_METHOD_CHOICES = ( 

74 # for HTML forms: value, description 

75 (IdNumValidationMethod.NONE, "None"), 

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

77) 

78 

79 

80def validate_id_number( 

81 req: "CamcopsRequest", idnum: Optional[int], method: str 

82) -> Tuple[bool, str]: 

83 """ 

84 Validates an ID number according to a method (as per 

85 :class:`IdNumValidationMethod`). 

86 

87 If the number is ``None``, that's valid (that's an ID policy failure, not 

88 a number validation failure). If ``method`` is falsy, that's also valid 

89 (no constraints). 

90 

91 Args: 

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

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

94 method: 

95 

96 Returns: 

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

98 ``why_invalid`` is ``str``. 

99 

100 """ 

101 _ = req.gettext 

102 if idnum is None or not method: 

103 return True, "" 

104 if not isinstance(idnum, int): 

105 return False, _("Not an integer") 

106 if method == IdNumValidationMethod.UK_NHS_NUMBER: 

107 if is_valid_nhs_number(idnum): 

108 return True, "" 

109 else: 

110 return False, _("Invalid UK NHS number") 

111 return False, _("Unknown validation method") 

112 

113 

114# ============================================================================= 

115# IdNumDefinition 

116# ============================================================================= 

117 

118 

119class IdNumDefinition(Base): 

120 """ 

121 Represents an ID number definition. 

122 """ 

123 

124 __tablename__ = "_idnum_definitions" 

125 

126 which_idnum = Column( 

127 "which_idnum", 

128 Integer, 

129 primary_key=True, 

130 index=True, 

131 comment="Which of the server's ID numbers is this?", 

132 ) 

133 description = Column( 

134 "description", 

135 IdDescriptorColType, 

136 comment="Full description of the ID number", 

137 ) 

138 short_description = Column( 

139 "short_description", 

140 IdDescriptorColType, 

141 comment="Short description of the ID number", 

142 ) 

143 hl7_id_type = Column( 

144 "hl7_id_type", 

145 HL7IdTypeType, 

146 comment="HL7: Identifier Type code: 'a code corresponding to the type " 

147 "of identifier. In some cases, this code may be used as a " 

148 'qualifier to the "Assigning Authority" component.\'', 

149 ) 

150 hl7_assigning_authority = Column( 

151 "hl7_assigning_authority", 

152 HL7AssigningAuthorityType, 

153 comment="HL7: Assigning Authority for ID number (unique name of the " 

154 "system/organization/agency/department that creates the data).", 

155 ) 

156 validation_method = Column( 

157 "validation_method", 

158 String(length=ID_NUM_VALIDATION_METHOD_MAX_LEN), 

159 comment="Optional validation method", 

160 ) 

161 fhir_id_system = Column( 

162 "fhir_id_system", UrlColType, comment="FHIR external ID 'system' URL" 

163 ) 

164 

165 def __init__( 

166 self, 

167 which_idnum: int = None, 

168 description: str = "", 

169 short_description: str = "", 

170 hl7_id_type: str = "", 

171 hl7_assigning_authority: str = "", 

172 validation_method: str = "", 

173 fhir_id_system: str = "", 

174 ): 

175 # We permit a "blank" constructor for automatic copying, e.g. merge_db. 

176 self.which_idnum = which_idnum 

177 self.description = description 

178 self.short_description = short_description 

179 self.hl7_id_type = hl7_id_type 

180 self.hl7_assigning_authority = hl7_assigning_authority 

181 self.validation_method = validation_method 

182 self.fhir_id_system = fhir_id_system 

183 

184 def __repr__(self) -> str: 

185 return simple_repr( 

186 self, 

187 ["which_idnum", "description", "short_description"], 

188 with_addr=False, 

189 ) 

190 

191 def _camcops_default_fhir_id_system(self, req: "CamcopsRequest") -> str: 

192 """ 

193 The built-in FHIR ID system URL that we'll use if the user hasn't 

194 specified one for the selected ID number type. 

195 """ 

196 return req.route_url( 

197 Routes.FHIR_PATIENT_ID_SYSTEM, which_idnum=self.which_idnum 

198 ) # path will be e.g. /fhir_patient_id_system/3 

199 

200 def effective_fhir_id_system(self, req: "CamcopsRequest") -> str: 

201 """ 

202 If the user has set a FHIR ID system, return that. Otherwise, return 

203 a CamCOPS default. 

204 """ 

205 return self.fhir_id_system or self._camcops_default_fhir_id_system(req) 

206 

207 def verbose_fhir_id_system(self, req: "CamcopsRequest") -> str: 

208 """ 

209 Returns a human-readable description of the FHIR ID system in effect, 

210 in HTML form. 

211 """ 

212 _ = req.gettext 

213 if self.fhir_id_system: 

214 prefix = "" 

215 url = self.fhir_id_system 

216 else: 

217 prefix = _("Default:") + " " 

218 url = self._camcops_default_fhir_id_system(req) 

219 return f'{prefix} <a href="{url}">{url}</a>' 

220 

221 

222# ============================================================================= 

223# Retrieving all IdNumDefinition objects 

224# ============================================================================= 

225 

226 

227def get_idnum_definitions(dbsession: SqlASession) -> List[IdNumDefinition]: 

228 """ 

229 Get all ID number definitions from the database, in order. 

230 """ 

231 return list( 

232 dbsession.query(IdNumDefinition).order_by(IdNumDefinition.which_idnum) 

233 )