Coverage for cc_modules/cc_idnumdef.py: 77%

1"""

2 camcops_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

30 import logging

31 from typing import List, Optional, Tuple, TYPE_CHECKING

32

33 from cardinal_pythonlib.logs import BraceStyleAdapter

34 from cardinal_pythonlib.nhs import is_valid_nhs_number

35 from cardinal_pythonlib.reprfunc import simple_repr

36 from sqlalchemy.orm import Mapped, mapped_column, Session as SqlASession

37 from sqlalchemy.sql.sqltypes import String

38

39 from camcops_server.cc_modules.cc_pyramid import Routes

40 from camcops_server.cc_modules.cc_sqla_coltypes import (

41 HL7AssigningAuthorityType,

42 HL7IdTypeType,

43 IdDescriptorColType,

44 UrlColType,

45)

46 from camcops_server.cc_modules.cc_sqlalchemy import Base

47

48 if TYPE_CHECKING:

49 from camcops_server.cc_modules.cc_request import CamcopsRequest

50

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

52

53

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

55# ID number validation

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

57

58 ID_NUM_VALIDATION_METHOD_MAX_LEN = 50

59

60

61 class 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

70 ID_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

77 def 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

116 class 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

217 def 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 )