Coverage for cc_modules/cc_patientidnum.py: 54%

79 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_patientidnum.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**Represent patient ID numbers.** 

29 

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

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

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

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

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

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

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

37Thus, always complete and contemporaneous. 

38 

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

40 overall_design.txt 

41 

42""" 

43 

44import logging 

45from typing import List, Tuple, TYPE_CHECKING 

46 

47from cardinal_pythonlib.logs import BraceStyleAdapter 

48from cardinal_pythonlib.reprfunc import simple_repr 

49from sqlalchemy.orm import relationship 

50from sqlalchemy.sql.schema import Column, ForeignKey 

51from sqlalchemy.sql.sqltypes import BigInteger, Integer 

52 

53from camcops_server.cc_modules.cc_constants import ( 

54 EXTRA_COMMENT_PREFIX, 

55 EXTRA_IDNUM_FIELD_PREFIX, 

56 NUMBER_OF_IDNUMS_DEFUNCT, 

57) 

58from camcops_server.cc_modules.cc_db import GenericTabletRecordMixin 

59from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition 

60from camcops_server.cc_modules.cc_simpleobjects import IdNumReference 

61from camcops_server.cc_modules.cc_sqla_coltypes import CamcopsColumn 

62from camcops_server.cc_modules.cc_sqlalchemy import Base 

63 

64if TYPE_CHECKING: 

65 from camcops_server.cc_modules.cc_patient import Patient 

66 from camcops_server.cc_modules.cc_request import CamcopsRequest 

67 

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

69 

70 

71# ============================================================================= 

72# PatientIdNum class 

73# ============================================================================= 

74# Stores ID numbers for a specific patient 

75 

76 

77class PatientIdNum(GenericTabletRecordMixin, Base): 

78 """ 

79 SQLAlchemy ORM class representing an ID number (as a 

80 which_idnum/idnum_value pair) for a patient. 

81 """ 

82 

83 __tablename__ = "patient_idnum" 

84 

85 id = Column( 

86 "id", 

87 Integer, 

88 nullable=False, 

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

90 ) 

91 patient_id = Column( 

92 "patient_id", 

93 Integer, 

94 nullable=False, 

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

96 ) 

97 which_idnum = Column( 

98 "which_idnum", 

99 Integer, 

100 ForeignKey(IdNumDefinition.which_idnum), 

101 nullable=False, 

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

103 ) 

104 idnum_value = CamcopsColumn( 

105 "idnum_value", 

106 BigInteger, 

107 identifies_patient=True, 

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

109 ) 

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

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

112 

113 patient = relationship( 

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

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

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

117 "Patient", 

118 primaryjoin=( 

119 "and_(" 

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

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

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

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

124 ")" 

125 ), 

126 uselist=False, 

127 viewonly=True, 

128 ) 

129 

130 # ------------------------------------------------------------------------- 

131 # String representations 

132 # ------------------------------------------------------------------------- 

133 

134 def __str__(self) -> str: 

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

136 

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

138 """ 

139 A prettified version of __str__. 

140 

141 Args: 

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

143 """ 

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

145 

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

147 """ 

148 A long-version prettified version of __str__. 

149 

150 Args: 

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

152 """ 

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

154 

155 def __repr__(self) -> str: 

156 return simple_repr( 

157 self, 

158 [ 

159 "_pk", 

160 "_device_id", 

161 "_era", 

162 "id", 

163 "patient_id", 

164 "which_idnum", 

165 "idnum_value", 

166 ], 

167 ) 

168 

169 # ------------------------------------------------------------------------- 

170 # Equality 

171 # ------------------------------------------------------------------------- 

172 

173 def __members(self) -> Tuple: 

174 """ 

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

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

177 """ # noqa 

178 return self.which_idnum, self.idnum_value 

179 

180 def __hash__(self) -> int: 

181 """ 

182 Must be compatible with __eq__. 

183 

184 See also 

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

186 """ # noqa 

187 return hash(self.__members()) 

188 

189 def __eq__(self, other: "PatientIdNum") -> bool: 

190 """ 

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

192 

193 Equivalent to: 

194 

195 .. code-block:: python 

196 

197 return ( 

198 self.which_idnum == other.which_idnum and 

199 self.idnum_value == other.idnum_value and 

200 self.which_idnum is not None and 

201 self.idnum_value is not None 

202 ) 

203 """ 

204 sm = self.__members() 

205 return ( 

206 type(self) is type(other) 

207 and (None not in sm) 

208 and sm == other.__members() 

209 ) 

210 

211 # ------------------------------------------------------------------------- 

212 # Validity 

213 # ------------------------------------------------------------------------- 

214 

215 def is_superficially_valid(self) -> bool: 

216 """ 

217 Is this a valid ID number? 

218 """ 

219 return ( 

220 self.which_idnum is not None 

221 and self.idnum_value is not None 

222 and self.which_idnum >= 0 

223 and self.idnum_value >= 0 

224 ) 

225 

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

227 if not self.is_superficially_valid(): 

228 return False 

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

230 

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

232 if not self.is_superficially_valid(): 

233 _ = req.gettext 

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

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

236 

237 # ------------------------------------------------------------------------- 

238 # ID type description 

239 # ------------------------------------------------------------------------- 

240 

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

242 """ 

243 Returns the full description for this ID number. 

244 """ 

245 which_idnum = self.which_idnum # type: int 

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

247 

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

249 """ 

250 Returns the short description for this ID number. 

251 """ 

252 which_idnum = self.which_idnum # type: int 

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

254 

255 # ------------------------------------------------------------------------- 

256 # Other representations 

257 # ------------------------------------------------------------------------- 

258 

259 def get_idnum_reference(self) -> IdNumReference: 

260 """ 

261 Returns an 

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

263 object summarizing this ID number. 

264 """ 

265 return IdNumReference( 

266 which_idnum=self.which_idnum, idnum_value=self.idnum_value 

267 ) 

268 

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

270 """ 

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

272 the number itself, for use in filenames. 

273 """ 

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

275 return "" 

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

277 

278 # ------------------------------------------------------------------------- 

279 # Set value 

280 # ------------------------------------------------------------------------- 

281 

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

283 """ 

284 Sets the ID number value. 

285 """ 

286 self.idnum_value = idnum_value 

287 

288 # ------------------------------------------------------------------------- 

289 # Patient 

290 # ------------------------------------------------------------------------- 

291 

292 def get_patient_server_pk(self) -> int: 

293 patient = self.patient # type: Patient 

294 if not patient: 

295 raise ValueError( 

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

297 ) 

298 return patient.pk 

299 

300 

301# ============================================================================= 

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

303# ============================================================================= 

304 

305 

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

307 """ 

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

309 use in upgrading old databases. 

310 """ 

311 return patient_id * NUMBER_OF_IDNUMS_DEFUNCT + which_idnum 

312 

313 

314# ============================================================================= 

315# Additional ID number column info for DB_PATIENT_ID_PER_ROW export option 

316# ============================================================================= 

317 

318 

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

320 """ 

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

322 ``DB_PATIENT_ID_PER_ROW`` export option. 

323 

324 Args: 

325 which_idnum: ID number type 

326 

327 Returns: 

328 str: ``idnum<which_idnum>`` 

329 

330 """ 

331 return f"{EXTRA_IDNUM_FIELD_PREFIX}{which_idnum}" 

332 

333 

334def extra_id_column(req: "CamcopsRequest", which_idnum: int) -> CamcopsColumn: 

335 """ 

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

337 ``DB_PATIENT_ID_PER_ROW`` export option. 

338 

339 Args: 

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

341 which_idnum: ID number type 

342 

343 Returns: 

344 the column definition 

345 

346 """ 

347 desc = req.get_id_desc(which_idnum) 

348 return CamcopsColumn( 

349 extra_id_colname(which_idnum), 

350 BigInteger, 

351 identifies_patient=True, 

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

353 ) 

354 

355 

356def all_extra_id_columns(req: "CamcopsRequest") -> List[CamcopsColumn]: 

357 """ 

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

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

360 

361 Args: 

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

363 

364 Returns: 

365 list: the column definitions 

366 """ 

367 return [ 

368 extra_id_column(req, which_idnum) 

369 for which_idnum in req.valid_which_idnums 

370 ]