Coverage for cc_modules/cc_simpleobjects.py: 58%

1"""

2 camcops_server/cc_modules/cc_simpleobjects.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**Simple struct-like classes.**

27

28"""

29

30 import copy

31 from typing import Any, List, Optional, TYPE_CHECKING

32

33 from pendulum import Date

34

35 from cardinal_pythonlib.datetimefunc import format_datetime

36 from cardinal_pythonlib.reprfunc import auto_repr

37

38 from camcops_server.cc_modules.cc_constants import DateFormat

39

40 if TYPE_CHECKING:

41 from camcops_server.cc_modules.cc_request import CamcopsRequest

42

43# Prefer classes to collections.namedtuple; both support type checking but

44# classes support better parameter checking (and refactoring) via PyCharm.

45

46

47# =============================================================================

48# IdNumReference

49# =============================================================================

50

51

52 class IdNumReference(object):

53 """

54 A simple way of referring to an ID number.

55

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

58

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

64

65 def __init__(self, which_idnum: int, idnum_value: int) -> None:

66 self.which_idnum = which_idnum

67 self.idnum_value = idnum_value

68

69 def __str__(self) -> str:

70 return f"idnum{self.which_idnum}={self.idnum_value}"

71

72 def __repr__(self) -> str:

73 return auto_repr(self)

74

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 )

82

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 )

90

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

95

96

97# =============================================================================

98# HL7PatientIdentifier

99# =============================================================================

100

101

102# noinspection PyShadowingBuiltins

103 class HL7PatientIdentifier(object):

104 """

105 Represents a patient identifier for the HL7 protocol.

106 """

107

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

116

117

118# =============================================================================

119# BarePatientInfo

120# =============================================================================

121

122

123 class BarePatientInfo(object):

124 """

125 Represents information about a patient using a simple object with no

126 connection to a database.

127

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

133

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]

157

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 )

176

177 def __repr__(self) -> str:

178 return auto_repr(self)

179

180 def add_idnum(self, idref: IdNumReference) -> None:

181 """

182 Adds an ID number. No checks in relation to what's already present.

183

184 Args:

185 idref: a :class:`IdNumReference`

186 """

187 self.idnum_definitions.append(idref)

188

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 )

206

207

208# =============================================================================

209# Raw XML value

210# =============================================================================

211

212

213 class XmlSimpleValue(object):

214 """

215 Represents XML lowest-level items. See functions in ``cc_xml.py``.

216 """

217

218 def __init__(self, value: Any) -> None:

219 self.value = value

220

221

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

223# TaskExportOptions

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

225

226

227 class TaskExportOptions(object):

228 """

229 Information-holding object for options controlling XML and other

230 representations of tasks.

231 """

232

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

258

259 include_blobs:

260 include binary large objects (BLOBs) (applies to several export

261 formats)

262

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

285

286 self.include_blobs = include_blobs

287

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

297

298 def clone(self) -> "TaskExportOptions":

299 """

300 Returns a copy of this object.

301 """

302 return copy.copy(self)