Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_patientidnum.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27**Represent patient ID numbers.** 

28 

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

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

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

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

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

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

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

36Thus, always complete and contemporaneous. 

37 

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

39 overall_design.txt 

40 

41""" 

42 

43import logging 

44from typing import List, Tuple, TYPE_CHECKING 

45 

46from cardinal_pythonlib.logs import BraceStyleAdapter 

47from cardinal_pythonlib.reprfunc import simple_repr 

48from sqlalchemy.orm import relationship 

49from sqlalchemy.sql.schema import Column, ForeignKey 

50from sqlalchemy.sql.sqltypes import BigInteger, Integer 

51 

52from camcops_server.cc_modules.cc_constants import ( 

53 EXTRA_COMMENT_PREFIX, 

54 EXTRA_IDNUM_FIELD_PREFIX, 

55 NUMBER_OF_IDNUMS_DEFUNCT, 

56) 

57from camcops_server.cc_modules.cc_db import GenericTabletRecordMixin 

58from camcops_server.cc_modules.cc_idnumdef import IdNumDefinition 

59from camcops_server.cc_modules.cc_simpleobjects import IdNumReference 

60from camcops_server.cc_modules.cc_sqla_coltypes import CamcopsColumn 

61from camcops_server.cc_modules.cc_sqlalchemy import Base 

62 

63if TYPE_CHECKING: 

64 from camcops_server.cc_modules.cc_patient import Patient 

65 from camcops_server.cc_modules.cc_request import CamcopsRequest 

66 

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

68 

69 

70# ============================================================================= 

71# PatientIdNum class 

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

73# Stores ID numbers for a specific patient 

74 

75class PatientIdNum(GenericTabletRecordMixin, Base): 

76 """ 

77 SQLAlchemy ORM class representing an ID number (as a 

78 which_idnum/idnum_value pair) for a patient. 

79 """ 

80 __tablename__ = "patient_idnum" 

81 

82 id = Column( 

83 "id", Integer, 

84 nullable=False, 

85 comment="Primary key on the source tablet device" 

86 ) 

87 patient_id = Column( 

88 "patient_id", Integer, 

89 nullable=False, 

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

91 ) 

92 which_idnum = Column( 

93 "which_idnum", Integer, ForeignKey(IdNumDefinition.which_idnum), 

94 nullable=False, 

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

96 ) 

97 idnum_value = CamcopsColumn( 

98 "idnum_value", 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 # http://docs.sqlalchemy.org/en/latest/orm/join_conditions.html#relationship-custom-foreign 

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

108 # http://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 # ------------------------------------------------------------------------- 

123 # String representations 

124 # ------------------------------------------------------------------------- 

125 

126 def __str__(self) -> str: 

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

128 

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

130 """ 

131 A prettified version of __str__. 

132 

133 Args: 

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

135 """ 

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

137 

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

139 """ 

140 A long-version prettified version of __str__. 

141 

142 Args: 

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

144 """ 

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

146 

147 def __repr__(self) -> str: 

148 return simple_repr(self, [ 

149 "_pk", "_device_id", "_era", 

150 "id", "patient_id", "which_idnum", "idnum_value" 

151 ]) 

152 

153 # ------------------------------------------------------------------------- 

154 # Equality 

155 # ------------------------------------------------------------------------- 

156 

157 def __members(self) -> Tuple: 

158 """ 

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

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

161 """ # noqa 

162 return self.which_idnum, self.idnum_value 

163 

164 def __hash__(self) -> int: 

165 """ 

166 Must be compatible with __eq__. 

167  

168 See also  

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

170 """ # noqa 

171 return hash(self.__members()) 

172 

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

174 """ 

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

176 

177 Equivalent to: 

178 

179 .. code-block:: python 

180 

181 return ( 

182 self.which_idnum == other.which_idnum and 

183 self.idnum_value == other.idnum_value and 

184 self.which_idnum is not None and 

185 self.idnum_value is not None 

186 ) 

187 """ 

188 sm = self.__members() 

189 return ( 

190 type(self) is type(other) and 

191 (None not in sm) and 

192 sm == other.__members() 

193 ) 

194 

195 # ------------------------------------------------------------------------- 

196 # Validity 

197 # ------------------------------------------------------------------------- 

198 

199 def is_superficially_valid(self) -> bool: 

200 """ 

201 Is this a valid ID number? 

202 """ 

203 return ( 

204 self.which_idnum is not None and 

205 self.idnum_value is not None and 

206 self.which_idnum >= 0 and 

207 self.idnum_value >= 0 

208 ) 

209 

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

211 if not self.is_superficially_valid(): 

212 return False 

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

214 

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

216 if not self.is_superficially_valid(): 

217 _ = req.gettext 

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

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

220 

221 # ------------------------------------------------------------------------- 

222 # ID type description 

223 # ------------------------------------------------------------------------- 

224 

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

226 """ 

227 Returns the full description for this ID number. 

228 """ 

229 which_idnum = self.which_idnum # type: int 

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

231 

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

233 """ 

234 Returns the short description for this ID number. 

235 """ 

236 which_idnum = self.which_idnum # type: int 

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

238 

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

240 # Other representations 

241 # ------------------------------------------------------------------------- 

242 

243 def get_idnum_reference(self) -> IdNumReference: 

244 """ 

245 Returns an 

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

247 object summarizing this ID number. 

248 """ 

249 return IdNumReference(which_idnum=self.which_idnum, 

250 idnum_value=self.idnum_value) 

251 

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

253 """ 

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

255 the number itself, for use in filenames. 

256 """ 

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

258 return "" 

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

260 

261 # ------------------------------------------------------------------------- 

262 # Set value 

263 # ------------------------------------------------------------------------- 

264 

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

266 """ 

267 Sets the ID number value. 

268 """ 

269 self.idnum_value = idnum_value 

270 

271 # ------------------------------------------------------------------------- 

272 # Patient 

273 # ------------------------------------------------------------------------- 

274 

275 def get_patient_server_pk(self) -> int: 

276 patient = self.patient # type: Patient 

277 if not patient: 

278 raise ValueError( 

279 "Corrupted database? PatientIdNum can't fetch its Patient") 

280 return patient.pk 

281 

282 

283# ============================================================================= 

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

285# ============================================================================= 

286 

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

288 """ 

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

290 use in upgrading old databases. 

291 """ 

292 return patient_id * NUMBER_OF_IDNUMS_DEFUNCT + which_idnum 

293 

294 

295# ============================================================================= 

296# Additional ID number column info for DB_PATIENT_ID_PER_ROW export option 

297# ============================================================================= 

298 

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

300 """ 

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

302 ``DB_PATIENT_ID_PER_ROW`` export option. 

303 

304 Args: 

305 which_idnum: ID number type 

306 

307 Returns: 

308 str: ``idnum<which_idnum>`` 

309 

310 """ 

311 return f"{EXTRA_IDNUM_FIELD_PREFIX}{which_idnum}" 

312 

313 

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

315 """ 

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

317 ``DB_PATIENT_ID_PER_ROW`` export option. 

318 

319 Args: 

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

321 which_idnum: ID number type 

322 

323 Returns: 

324 the column definition 

325 

326 """ 

327 desc = req.get_id_desc(which_idnum) 

328 return CamcopsColumn( 

329 extra_id_colname(which_idnum), 

330 BigInteger, 

331 identifies_patient=True, 

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

333 ) 

334 

335 

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

337 """ 

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

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

340 

341 Args: 

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

343 

344 Returns: 

345 list: the column definitions 

346 """ 

347 return [ 

348 extra_id_column(req, which_idnum) 

349 for which_idnum in req.valid_which_idnums 

350 ]