Coverage for cc_modules/cc_patientidnum.py: 57%

1"""

2 camcops_server/cc_modules/cc_patientidnum.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**Represent patient ID numbers.**

27

28 We were looking up ID descriptors from the device's stored variables.

29 However, that is a bit of a nuisance for a server-side researcher, and

30 it's a pain to copy the server's storedvar values (and -- all or some?)

31 when a patient gets individually moved off the tablet. Anyway, they're

32 important, so a little repetition is not the end of the world. So,

33 let's have the tablet store its current ID descriptors in the patient

34 record at the point of upload, and then it's available here directly.

35 Thus, always complete and contemporaneous.

36

37... DECISION CHANGED 2017-07-08; see justification in tablet

38 overall_design.txt

39

40"""

41

42 import logging

43 from typing import List, Optional, Tuple, TYPE_CHECKING

44

45 from cardinal_pythonlib.logs import BraceStyleAdapter

46 from cardinal_pythonlib.reprfunc import simple_repr

47 from sqlalchemy.orm import mapped_column, Mapped, relationship

48 from sqlalchemy.sql.schema import Column, ForeignKey

49 from sqlalchemy.sql.sqltypes import BigInteger

50

51 from camcops_server.cc_modules.cc_constants import (

52 EXTRA_COMMENT_PREFIX,

53 EXTRA_IDNUM_FIELD_PREFIX,

54 NUMBER_OF_IDNUMS_DEFUNCT,

55)

56 from camcops_server.cc_modules.cc_db import GenericTabletRecordMixin

57 from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition

58 from camcops_server.cc_modules.cc_simpleobjects import IdNumReference

59 from camcops_server.cc_modules.cc_sqla_coltypes import (

60 mapped_camcops_column,

61 camcops_column,

62)

63 from camcops_server.cc_modules.cc_sqlalchemy import Base

64

65 if TYPE_CHECKING:

66 from camcops_server.cc_modules.cc_patient import Patient

67 from camcops_server.cc_modules.cc_request import CamcopsRequest

68

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

70

71

72# =============================================================================

73# PatientIdNum class

74# =============================================================================

75# Stores ID numbers for a specific patient

76

77

78 class PatientIdNum(GenericTabletRecordMixin, Base):

79 """

80 SQLAlchemy ORM class representing an ID number (as a

81 which_idnum/idnum_value pair) for a patient.

82 """

83

84 __tablename__ = "patient_idnum"

85

86 id: Mapped[int] = mapped_column(

87 comment="Primary key on the source tablet device",

88 )

89 patient_id: Mapped[int] = mapped_column(

90 comment="FK to patient.id (for this device/era)",

91 )

92 which_idnum: Mapped[int] = mapped_column(

93 ForeignKey(IdNumDefinition.which_idnum),

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

95 )

96 idnum_value: Mapped[Optional[int]] = mapped_camcops_column(

97 "idnum_value",

98 BigInteger,

99 identifies_patient=True,

100 comment="The value of the ID number",

101 )

102 # Note: we don't use a relationship() to IdNumDefinition here; we do that

103 # sort of work via the CamcopsRequest, which caches them for speed.

104

105 patient = relationship(

106 # https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#relationship-custom-foreign

107 # https://docs.sqlalchemy.org/en/latest/orm/relationship_api.html#sqlalchemy.orm.relationship # noqa

108 # https://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#relationship-primaryjoin # noqa

109 "Patient",

110 primaryjoin=(

111 "and_("

112 " remote(Patient.id) == foreign(PatientIdNum.patient_id), "

113 " remote(Patient._device_id) == foreign(PatientIdNum._device_id), "

114 " remote(Patient._era) == foreign(PatientIdNum._era), "

115 " remote(Patient._current) == True "

116 ")"

117 ),

118 uselist=False,

119 viewonly=True,

120 )

121

122 duplicates: Mapped[list["PatientIdNum"]] = relationship(

123 primaryjoin=(

124 "and_("

125 " remote(PatientIdNum._pk) != foreign(PatientIdNum._pk), "

126 " remote(PatientIdNum._group_id) == foreign(PatientIdNum._group_id), " # noqa: E501

127 " remote(PatientIdNum.which_idnum) == foreign(PatientIdNum.which_idnum), " # noqa: E501

128 " remote(PatientIdNum.idnum_value) == foreign(PatientIdNum.idnum_value), " # noqa: E501

129 " remote(PatientIdNum._device_id) == foreign(PatientIdNum._device_id), " # noqa: E501

130 " remote(PatientIdNum._era) == foreign(PatientIdNum._era), "

131 " remote(PatientIdNum._current) == True, "

132 ")"

133 ),

134 viewonly=True,

135 uselist=True,

136 )

137

138 # -------------------------------------------------------------------------

139 # String representations

140 # -------------------------------------------------------------------------

141

142 def __str__(self) -> str:

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

144

145 def prettystr(self, req: "CamcopsRequest") -> str:

146 """

147 A prettified version of __str__.

148

149 Args:

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

151 """

152 return f"{self.short_description(req)} {self.idnum_value}"

153

154 def full_prettystr(self, req: "CamcopsRequest") -> str:

155 """

156 A long-version prettified version of __str__.

157

158 Args:

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

160 """

161 return f"{self.description(req)} {self.idnum_value}"

162

163 def __repr__(self) -> str:

164 return simple_repr(

165 self,

166 [

167 "_pk",

168 "_device_id",

169 "_era",

170 "id",

171 "patient_id",

172 "which_idnum",

173 "idnum_value",

174 ],

175 )

176

177 # -------------------------------------------------------------------------

178 # Equality

179 # -------------------------------------------------------------------------

180

181 def __members(self) -> Tuple:

182 """

183 For :meth:`__hash__` and :meth:`__eq__`, as per

184 https://stackoverflow.com/questions/45164691/recommended-way-to-implement-eq-and-hash

185 """

186 return self.which_idnum, self.idnum_value

187

188 def __hash__(self) -> int:

189 """

190 Must be compatible with __eq__.

191

192 See also

193 https://stackoverflow.com/questions/45164691/recommended-way-to-implement-eq-and-hash

194 """

195 return hash(self.__members())

196

197 def __eq__(self, other: object) -> bool:

198 """

199 Do ``self`` and ``other`` represent the same ID number?

200

201 Equivalent to:

202

203 .. code-block:: python

204

205 return (

206 self.which_idnum == other.which_idnum and

207 self.idnum_value == other.idnum_value and

208 self.which_idnum is not None and

209 self.idnum_value is not None

210 )

211 """

212 if not isinstance(other, PatientIdNum):

213 return NotImplemented

214

215 sm = self.__members()

216 return (None not in sm) and sm == other.__members()

217

218 # -------------------------------------------------------------------------

219 # Validity

220 # -------------------------------------------------------------------------

221

222 def is_superficially_valid(self) -> bool:

223 """

224 Is this a valid ID number?

225 """

226 return (

227 self.which_idnum is not None

228 and self.idnum_value is not None

229 and self.which_idnum >= 0

230 and self.idnum_value >= 0

231 )

232

233 def is_fully_valid(self, req: "CamcopsRequest") -> bool:

234 if not self.is_superficially_valid():

235 return False

236 return req.is_idnum_valid(self.which_idnum, self.idnum_value)

237

238 def why_invalid(self, req: "CamcopsRequest") -> str:

239 if not self.is_superficially_valid():

240 _ = req.gettext

241 return _("ID number fails basic checks")

242 return req.why_idnum_invalid(self.which_idnum, self.idnum_value)

243

244 # -------------------------------------------------------------------------

245 # ID type description

246 # -------------------------------------------------------------------------

247

248 def description(self, req: "CamcopsRequest") -> str:

249 """

250 Returns the full description for this ID number.

251 """

252 which_idnum = self.which_idnum # type: int

253 return req.get_id_desc(which_idnum, default="?")

254

255 def short_description(self, req: "CamcopsRequest") -> str:

256 """

257 Returns the short description for this ID number.

258 """

259 which_idnum = self.which_idnum # type: int

260 return req.get_id_shortdesc(which_idnum, default="?")

261

262 # -------------------------------------------------------------------------

263 # Other representations

264 # -------------------------------------------------------------------------

265

266 def get_idnum_reference(self) -> IdNumReference:

267 """

268 Returns an

269 :class:`camcops_server.cc_modules.cc_simpleobjects.IdNumReference`

270 object summarizing this ID number.

271 """

272 return IdNumReference(

273 which_idnum=self.which_idnum, idnum_value=self.idnum_value

274 )

275

276 def get_filename_component(self, req: "CamcopsRequest") -> str:

277 """

278 Returns a string including the short description of the ID number, and

279 the number itself, for use in filenames.

280 """

281 if self.which_idnum is None or self.idnum_value is None:

282 return ""

283 return f"{self.short_description(req)}-{self.idnum_value}"

284

285 # -------------------------------------------------------------------------

286 # Set value

287 # -------------------------------------------------------------------------

288

289 def set_idnum(self, idnum_value: int) -> None:

290 """

291 Sets the ID number value.

292 """

293 self.idnum_value = idnum_value

294

295 # -------------------------------------------------------------------------

296 # Patient

297 # -------------------------------------------------------------------------

298

299 def get_patient_server_pk(self) -> int:

300 patient = self.patient # type: Patient

301 if not patient:

302 raise ValueError(

303 "Corrupted database? PatientIdNum can't fetch its Patient"

304 )

305 return patient.pk

306

307

308# =============================================================================

309# Fake ID values when upgrading from old ID number system

310# =============================================================================

311

312

313 def fake_tablet_id_for_patientidnum(patient_id: int, which_idnum: int) -> int:

314 """

315 Returns a fake client-side PK (tablet ID) for a patient number. Only for

316 use in upgrading old databases.

317 """

318 return patient_id * NUMBER_OF_IDNUMS_DEFUNCT + which_idnum

319

320

321# =============================================================================

322# Additional ID number column info for DB_PATIENT_ID_PER_ROW export option

323# =============================================================================

324

325

326 def extra_id_colname(which_idnum: int) -> str:

327 """

328 The column name used for the extra ID number columns provided by the

329 ``DB_PATIENT_ID_PER_ROW`` export option.

330

331 Args:

332 which_idnum: ID number type

333

334 Returns:

335 str: ``idnum<which_idnum>``

336

337 """

338 return f"{EXTRA_IDNUM_FIELD_PREFIX}{which_idnum}"

339

340

341 def extra_id_column(req: "CamcopsRequest", which_idnum: int) -> Column:

342 """

343 The column definition used for the extra ID number columns provided by the

344 ``DB_PATIENT_ID_PER_ROW`` export option.

345

346 Args:

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

348 which_idnum: ID number type

349

350 Returns:

351 the column definition

352

353 """

354 desc = req.get_id_desc(which_idnum)

355 return camcops_column(

356 extra_id_colname(which_idnum),

357 BigInteger,

358 identifies_patient=True,

359 comment=EXTRA_COMMENT_PREFIX + f"ID number {which_idnum}: {desc}",

360 )

361

362

363 def all_extra_id_columns(req: "CamcopsRequest") -> List[Column]:

364 """

365 Returns all column definitions used for the extra ID number columns

366 provided by the ``DB_PATIENT_ID_PER_ROW`` export option.

367

368 Args:

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

370

371 Returns:

372 list: the column definitions

373 """

374 return [

375 extra_id_column(req, which_idnum)

376 for which_idnum in req.valid_which_idnums

377 ]