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_policy.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**Represents ID number policies.** 

28 

29Note that the upload script should NOT attempt to verify patients against the 

30ID policy, not least because tablets are allowed to upload task data (in a 

31separate transaction) before uploading patients; referential integrity would be 

32very hard to police. So the tablet software deals with ID compliance. (Also, 

33the superuser can change the server's ID policy retrospectively!) 

34 

35Both the client and the server do policy tokenizing and can check patient info 

36against policies. The server has additional code to answer questions like "is 

37this policy valid?" (in general and in the context of the server's 

38configuration). 

39 

40""" 

41 

42import io 

43import logging 

44import tokenize 

45from typing import Callable, Dict, List, Optional, Tuple 

46 

47from cardinal_pythonlib.dicts import reversedict 

48from cardinal_pythonlib.logs import BraceStyleAdapter 

49from cardinal_pythonlib.reprfunc import auto_repr 

50 

51from camcops_server.cc_modules.cc_simpleobjects import BarePatientInfo 

52 

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

54 

55 

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

57# Tokens 

58# ============================================================================= 

59 

60TOKEN_TYPE = int 

61TOKENIZED_POLICY_TYPE = List[TOKEN_TYPE] 

62 

63# http://stackoverflow.com/questions/36932 

64BAD_TOKEN = 0 

65TK_LPAREN = -1 

66TK_RPAREN = -2 

67TK_AND = -3 

68TK_OR = -4 

69TK_NOT = -5 

70TK_ANY_IDNUM = -6 

71TK_OTHER_IDNUM = -7 

72TK_FORENAME = -8 

73TK_SURNAME = -9 

74TK_SEX = -10 

75TK_DOB = -11 

76TK_ADDRESS = -12 

77TK_GP = -13 

78TK_OTHER_DETAILS = -14 

79TK_EMAIL = -15 

80 

81# Tokens for ID numbers are from 1 upwards. 

82 

83POLICY_TOKEN_DICT = { 

84 "(": TK_LPAREN, 

85 ")": TK_RPAREN, 

86 "AND": TK_AND, 

87 "OR": TK_OR, 

88 "NOT": TK_NOT, 

89 

90 "ANYIDNUM": TK_ANY_IDNUM, 

91 "OTHERIDNUM": TK_OTHER_IDNUM, 

92 

93 "FORENAME": TK_FORENAME, 

94 "SURNAME": TK_SURNAME, 

95 "SEX": TK_SEX, 

96 "DOB": TK_DOB, 

97 "ADDRESS": TK_ADDRESS, 

98 "GP": TK_GP, 

99 "OTHERDETAILS": TK_OTHER_DETAILS, 

100 "EMAIL": TK_EMAIL, 

101} 

102TOKEN_POLICY_DICT = reversedict(POLICY_TOKEN_DICT) 

103 

104NON_IDNUM_INFO_TOKENS = [ 

105 TK_OTHER_IDNUM, TK_ANY_IDNUM, 

106 TK_FORENAME, TK_SURNAME, TK_SEX, TK_DOB, 

107 TK_ADDRESS, TK_GP, TK_OTHER_DETAILS, TK_EMAIL, 

108] 

109 

110TOKEN_IDNUM_PREFIX = "IDNUM" 

111 

112 

113def is_info_token(token: int) -> bool: 

114 """ 

115 Is the token a kind that represents information, not (for example) an 

116 operator? 

117 """ 

118 return token > 0 or token in NON_IDNUM_INFO_TOKENS 

119 

120 

121def token_to_str(token: int) -> str: 

122 """ 

123 Returns a string version of the specified token. 

124 """ 

125 if token < 0: 

126 return TOKEN_POLICY_DICT.get(token) 

127 else: 

128 return TOKEN_IDNUM_PREFIX + str(token) 

129 

130 

131# ============================================================================= 

132# Quad-state logic 

133# ============================================================================= 

134 

135class QuadState(object): 

136 def __str__(self) -> str: 

137 if self is Q_TRUE: 

138 return "QTrue" 

139 elif self is Q_FALSE: 

140 return "QFalse" 

141 elif self is Q_DONT_CARE: 

142 return "QDontCare" 

143 else: 

144 return "QError" 

145 

146 

147Q_TRUE = QuadState() 

148Q_FALSE = QuadState() 

149Q_ERROR = QuadState() 

150Q_DONT_CARE = QuadState() 

151 

152 

153def bool_to_quad(x: bool) -> QuadState: 

154 return Q_TRUE if x else Q_FALSE 

155 

156 

157def quad_not(x: QuadState) -> QuadState: 

158 # Boolean logic 

159 if x is Q_TRUE: 

160 return Q_FALSE 

161 elif x is Q_FALSE: 

162 return Q_TRUE 

163 # Unusual logic 

164 elif x is Q_DONT_CARE: 

165 return Q_DONT_CARE 

166 else: 

167 return Q_ERROR 

168 

169 

170def quad_and(x: QuadState, y: QuadState) -> QuadState: 

171 either = (x, y) 

172 # Unusual logic 

173 if Q_ERROR in either: 

174 return Q_ERROR 

175 elif Q_DONT_CARE in either: 

176 other = either[1] if either[0] == Q_DONT_CARE else either[0] 

177 return other 

178 # Boolean logic 

179 elif x is Q_TRUE and y is Q_TRUE: 

180 return Q_TRUE 

181 else: 

182 return Q_FALSE 

183 

184 

185def quad_or(x: QuadState, y: QuadState) -> QuadState: 

186 either = (x, y) 

187 # Unusual logic 

188 if Q_ERROR in either: 

189 return Q_ERROR 

190 elif Q_DONT_CARE in either: 

191 other = either[1] if either[0] == Q_DONT_CARE else either[0] 

192 return other 

193 # Boolean logic 

194 elif x is Q_TRUE or y is Q_TRUE: 

195 return Q_TRUE 

196 else: 

197 return Q_FALSE 

198 

199 

200def debug_wrapper(fn: Callable, name: str) -> Callable: 

201 def wrap(*args, **kwargs) -> QuadState: 

202 result = fn(*args, **kwargs) 

203 arglist = [str(x) for x in args] + [f"{k}={v}" 

204 for k, v in kwargs.items()] 

205 log.critical("{}({}) -> {}".format(name, ", ".join(arglist), result)) 

206 return result 

207 return wrap 

208 

209 

210DEBUG_QUAD_STATE_LOGIC = False 

211 

212if DEBUG_QUAD_STATE_LOGIC: 

213 quad_not = debug_wrapper(quad_not, "quad_not") 

214 quad_and = debug_wrapper(quad_and, "quad_and") 

215 quad_or = debug_wrapper(quad_or, "quad_or") 

216 

217 

218# ============================================================================= 

219# PatientInfoPresence 

220# ============================================================================= 

221 

222class PatientInfoPresence(object): 

223 """ 

224 Represents simply the presence/absence of different kinds of information 

225 about a patient. 

226 """ 

227 def __init__(self, 

228 present: Dict[int, QuadState] = None, 

229 default: QuadState = Q_FALSE) -> None: 

230 """ 

231 Args: 

232 present: map from token to :class:`QuadState` 

233 default: default :class:`QuadState` to return if unspecified 

234 """ 

235 self.present = present or {} # type: Dict[int, QuadState] 

236 self.default = default 

237 for t in self.present.keys(): 

238 assert is_info_token(t) 

239 

240 def __repr__(self) -> str: 

241 return auto_repr(self) 

242 

243 def is_present(self, token: int, 

244 default: QuadState = None) -> QuadState: 

245 """ 

246 Is information represented by a particular token present? 

247 

248 Args: 

249 token: token to check for; e.g. :data:`TK_FORENAME` 

250 default: default :class:`QuadState` to return if unspecified; if 

251 this is None, ``self.default`` is used. 

252 

253 Returns: 

254 a :class:`QuadState` 

255 """ 

256 return self.present.get(token, default or self.default) 

257 

258 @property 

259 def forename_present(self) -> QuadState: 

260 return self.is_present(TK_FORENAME) 

261 

262 @property 

263 def surname_present(self) -> QuadState: 

264 return self.is_present(TK_SURNAME) 

265 

266 @property 

267 def sex_present(self) -> QuadState: 

268 return self.is_present(TK_SEX) 

269 

270 @property 

271 def dob_present(self) -> QuadState: 

272 return self.is_present(TK_DOB) 

273 

274 @property 

275 def address_present(self) -> QuadState: 

276 return self.is_present(TK_ADDRESS) 

277 

278 @property 

279 def email_present(self) -> QuadState: 

280 return self.is_present(TK_EMAIL) 

281 

282 @property 

283 def gp_present(self) -> QuadState: 

284 return self.is_present(TK_GP) 

285 

286 @property 

287 def otherdetails_present(self) -> QuadState: 

288 return self.is_present(TK_OTHER_DETAILS) 

289 

290 @property 

291 def otheridnum_present(self) -> QuadState: 

292 return self.is_present(TK_OTHER_IDNUM) 

293 

294 @property 

295 def special_anyidnum_present(self) -> QuadState: 

296 return self.is_present(TK_ANY_IDNUM) 

297 

298 def idnum_present(self, which_idnum: int) -> QuadState: 

299 """ 

300 Is the specified ID number type present? 

301 """ 

302 assert which_idnum > 0 

303 return self.is_present(which_idnum) 

304 

305 def any_idnum_present(self) -> QuadState: 

306 """ 

307 Is at least one ID number present? 

308 """ 

309 for k, v in self.present.items(): 

310 if k > 0 and v is Q_TRUE: 

311 return Q_TRUE 

312 return self.special_anyidnum_present 

313 

314 @classmethod 

315 def make_from_ptinfo( 

316 cls, 

317 ptinfo: BarePatientInfo, 

318 policy_mentioned_idnums: List[int]) -> "PatientInfoPresence": 

319 """ 

320 Returns a :class:`PatientInfoPresence` representing whether different 

321 kinds of information about the patient are present or not. 

322 """ 

323 presences = { 

324 TK_FORENAME: bool_to_quad(bool(ptinfo.forename)), 

325 TK_SURNAME: bool_to_quad(bool(ptinfo.surname)), 

326 TK_SEX: bool_to_quad(bool(ptinfo.sex)), 

327 TK_DOB: bool_to_quad(ptinfo.dob is not None), 

328 TK_ADDRESS: bool_to_quad(bool(ptinfo.address)), 

329 TK_EMAIL: bool_to_quad(bool(ptinfo.email)), 

330 TK_GP: bool_to_quad(bool(ptinfo.gp)), 

331 TK_OTHER_DETAILS: bool_to_quad(bool(ptinfo.otherdetails)), 

332 TK_OTHER_IDNUM: Q_FALSE, # may change 

333 } # type: Dict[int, QuadState] 

334 for iddef in ptinfo.idnum_definitions: 

335 this_idnum_present = iddef.idnum_value is not None 

336 presences[iddef.which_idnum] = bool_to_quad(this_idnum_present) 

337 if iddef.which_idnum not in policy_mentioned_idnums: 

338 presences[TK_OTHER_IDNUM] = Q_TRUE 

339 return cls(presences, default=Q_FALSE) 

340 

341 @classmethod 

342 def make_uncaring(cls) -> "PatientInfoPresence": 

343 """ 

344 Makes a :class:`PatientInfoPresence` that doesn't care about anything. 

345 """ 

346 return cls({}, default=Q_DONT_CARE) 

347 

348 def set_idnum_presence(self, which_idnum: int, present: QuadState) -> None: 

349 """ 

350 Set the "presence" state for one ID number type. 

351 

352 Args: 

353 which_idnum: which ID number type 

354 present: its state of being present (or not, or other states) 

355 """ 

356 self.present[which_idnum] = present 

357 

358 @classmethod 

359 def make_uncaring_except(cls, token: int, 

360 present: QuadState) -> "PatientInfoPresence": 

361 """ 

362 Make a :class:`PatientInfoPresence` that is uncaring except for one 

363 thing, specified by token. 

364 """ 

365 assert is_info_token(token) 

366 pip = cls.make_uncaring() 

367 pip.present[token] = present 

368 return pip 

369 

370 

371# ============================================================================= 

372# More constants 

373# ============================================================================= 

374 

375CONTENT_TOKEN_PROCESSOR_TYPE = Callable[[int], QuadState] 

376 

377 

378# ============================================================================= 

379# TokenizedPolicy 

380# ============================================================================= 

381 

382class TokenizedPolicy(object): 

383 """ 

384 Represents a tokenized ID policy. 

385 

386 A tokenized policy is a policy represented by a sequence of integers; 

387 0 means "bad token"; negative numbers represent fixed things like 

388 "forename" or "left parenthesis" or "and"; positive numbers represent 

389 ID number types. 

390 """ 

391 def __init__(self, policy: str) -> None: 

392 self.tokens = self.get_tokenized_id_policy(policy) 

393 self._syntactically_valid = None # type: Optional[bool] 

394 self.valid_idnums = None # type: Optional[List[int]] 

395 self._valid_for_idnums = None # type: Optional[bool] 

396 

397 def __str__(self) -> str: 

398 policy = " ".join(token_to_str(t) for t in self.tokens) 

399 policy = policy.replace("( ", "(") 

400 policy = policy.replace(" )", ")") 

401 return policy 

402 

403 # ------------------------------------------------------------------------- 

404 # ID number info 

405 # ------------------------------------------------------------------------- 

406 

407 def set_valid_idnums(self, valid_idnums: List[int]) -> None: 

408 """ 

409 Make a note of which ID number types are currently valid. 

410 Caches "valid for these ID numbers" information. 

411 

412 Args: 

413 valid_idnums: list of valid ID number types 

414 """ 

415 sorted_idnums = sorted(valid_idnums) 

416 if sorted_idnums != self.valid_idnums: 

417 self.valid_idnums = sorted_idnums 

418 self._valid_for_idnums = None # clear cache 

419 

420 def require_valid_idnum_info(self) -> None: 

421 """ 

422 Checks that set_valid_idnums() has been called properly, or raises 

423 :exc:`AssertionError`. 

424 """ 

425 assert self.valid_idnums is not None, ( 

426 "Must call set_valid_idnums() first! Currently: {!r}" 

427 ) 

428 

429 # ------------------------------------------------------------------------- 

430 # Tokenize 

431 # ------------------------------------------------------------------------- 

432 

433 @staticmethod 

434 def name_to_token(name: str) -> int: 

435 """ 

436 Converts an upper-case string token name (such as ``DOB``) to an 

437 integer token. 

438 """ 

439 if name in POLICY_TOKEN_DICT: 

440 return POLICY_TOKEN_DICT[name] 

441 if name.startswith(TOKEN_IDNUM_PREFIX): 

442 nstr = name[len(TOKEN_IDNUM_PREFIX):] 

443 try: 

444 return int(nstr) 

445 except (TypeError, ValueError): 

446 return BAD_TOKEN 

447 return BAD_TOKEN 

448 

449 @classmethod 

450 def get_tokenized_id_policy(cls, policy: str) \ 

451 -> TOKENIZED_POLICY_TYPE: 

452 """ 

453 Takes a string policy and returns a tokenized policy, meaning a list of 

454 integer tokens, or ``[]``. 

455 """ 

456 if policy is None: 

457 return [] 

458 # http://stackoverflow.com/questions/88613 

459 string_index = 1 

460 # single line, upper case: 

461 policy = " ".join(policy.strip().upper().splitlines()) 

462 try: 

463 tokenstrings = list( 

464 token[string_index] 

465 for token in tokenize.generate_tokens( 

466 io.StringIO(policy).readline) 

467 if token[string_index] 

468 ) 

469 except tokenize.TokenError: 

470 # something went wrong 

471 return [] 

472 tokens = [cls.name_to_token(k) for k in tokenstrings] 

473 if any(t == BAD_TOKEN for t in tokens): 

474 # There's something bad in there. 

475 return [] 

476 return tokens 

477 

478 # ------------------------------------------------------------------------- 

479 # Validity checks 

480 # ------------------------------------------------------------------------- 

481 

482 def is_syntactically_valid(self) -> bool: 

483 """ 

484 Is the policy syntactically valid? This is a basic check. 

485 """ 

486 if self._syntactically_valid is None: 

487 # Cache it 

488 if not self.tokens: 

489 self._syntactically_valid = False 

490 else: 

491 # Evaluate against a dummy patient info object. If we get None, 

492 # it's gone wrong. 

493 pip = PatientInfoPresence() 

494 value = self._value_for_pip(pip) 

495 self._syntactically_valid = value is not Q_ERROR 

496 return self._syntactically_valid 

497 

498 def is_valid(self, valid_idnums: List[int] = None, 

499 verbose: bool = False) -> bool: 

500 """ 

501 Is the policy valid in the context of the ID types available in our 

502 database? 

503 

504 Args: 

505 valid_idnums: optional list of valid ID number types 

506 verbose: report reasons to debug log 

507 """ 

508 if valid_idnums is not None: 

509 self.set_valid_idnums(valid_idnums) 

510 if self._valid_for_idnums is None: 

511 # Cache information 

512 self.require_valid_idnum_info() 

513 self._valid_for_idnums = self.is_valid_for_idnums( 

514 self.valid_idnums, verbose=verbose) 

515 return self._valid_for_idnums 

516 

517 def is_valid_for_idnums(self, valid_idnums: List[int], 

518 verbose: bool = False) -> bool: 

519 """ 

520 Is the policy valid, given a list of valid ID number types? 

521 

522 Checks the following: 

523 

524 - valid syntax 

525 - refers only to ID number types defined on the server 

526 - is compatible with the tablet ID policy 

527 

528 Args: 

529 valid_idnums: ID number types that are valid on the server 

530 verbose: report reasons to debug log 

531 """ 

532 # First, syntax: 

533 if not self.is_syntactically_valid(): 

534 if verbose: 

535 log.debug("is_valid_for_idnums(): Not syntactically valid") 

536 return False 

537 # Second, all ID numbers referred to by the policy exist: 

538 for token in self.tokens: 

539 if token > 0 and token not in valid_idnums: 

540 if verbose: 

541 log.debug("is_valid_for_idnums(): Refers to ID number type " 

542 "{}, which does not exist", token) 

543 return False 

544 if not self._compatible_with_tablet_id_policy(verbose=verbose): 

545 if verbose: 

546 log.debug("is_valid_for_idnums(): Less restrictive than the " 

547 "tablet minimum ID policy; invalid") 

548 return False 

549 return True 

550 

551 # ------------------------------------------------------------------------- 

552 # Information about the ID number types the policy refers to 

553 # ------------------------------------------------------------------------- 

554 

555 def relevant_idnums(self, valid_idnums: List[int]) -> List[int]: 

556 """ 

557 Which ID numbers are relevant to this policy? 

558 

559 Args: 

560 valid_idnums: ID number types that are valid on the server 

561 

562 Returns: 

563 the subset of ``valid_idnums`` that is mentioned somehow in the 

564 policy 

565 """ 

566 if not self.tokens: 

567 return [] 

568 if TK_ANY_IDNUM in self.tokens or TK_OTHER_IDNUM in self.tokens: 

569 # all are relevant 

570 return valid_idnums 

571 relevant_idnums = [] # type: List[int] 

572 for which_idnum in valid_idnums: 

573 assert which_idnum > 0, "Silly ID number types" 

574 if which_idnum in self.tokens: 

575 relevant_idnums.append(which_idnum) 

576 return relevant_idnums 

577 

578 def specifically_mentioned_idnums(self) -> List[int]: 

579 """ 

580 Returns the ID number tokens for all ID numbers mentioned in the 

581 policy, as a list. 

582 """ 

583 return [x for x in self.tokens if x > 0] 

584 

585 def contains_specific_idnum(self, which_idnum: int) -> bool: 

586 """ 

587 Does the policy refer specifically to the given ID number type? 

588 

589 Args: 

590 which_idnum: ID number type to test 

591 """ 

592 assert which_idnum > 0 

593 return which_idnum in self.tokens 

594 

595 # ------------------------------------------------------------------------- 

596 # More complex attributes 

597 # ------------------------------------------------------------------------- 

598 

599 def find_critical_single_numerical_id( 

600 self, 

601 valid_idnums: List[int] = None, 

602 verbose: bool = False) -> Optional[int]: 

603 """ 

604 If the policy involves a single mandatory ID number, return that ID 

605 number; otherwise return None. 

606 

607 Args: 

608 valid_idnums: ID number types that are valid on the server 

609 verbose: report reasons to debug log 

610 

611 Returns: 

612 int: the single critical ID number type, or ``None`` 

613 """ 

614 if not self.is_valid(valid_idnums): 

615 if verbose: 

616 log.debug("find_critical_single_numerical_id(): invalid") 

617 return None 

618 relevant_idnums = self.specifically_mentioned_idnums() 

619 possible_critical_idnums = [] # type: List[int] 

620 for which_idnum in relevant_idnums: 

621 pip_with = PatientInfoPresence.make_uncaring_except(which_idnum, 

622 Q_TRUE) 

623 satisfies_with_1 = self._value_for_pip(pip_with) 

624 pip_with.present[TK_OTHER_IDNUM] = Q_FALSE 

625 satisfies_with_2 = self._value_for_pip(pip_with) 

626 pip_without = PatientInfoPresence.make_uncaring_except(which_idnum, 

627 Q_FALSE) 

628 satisfies_without_1 = self._value_for_pip(pip_without) 

629 pip_with.present[TK_OTHER_IDNUM] = Q_TRUE 

630 satisfies_without_2 = self._value_for_pip(pip_without) 

631 if verbose: 

632 log.debug( 

633 "... {}: satisfies_with={}, satisfies_without_1={}, " 

634 "satisfies_without_2={}", 

635 which_idnum, satisfies_with_1, satisfies_without_1, 

636 satisfies_without_2, 

637 ) 

638 if (satisfies_with_1 is Q_TRUE and 

639 satisfies_with_2 is Q_TRUE and 

640 satisfies_without_1 is Q_FALSE and 

641 satisfies_without_2 is Q_FALSE): 

642 possible_critical_idnums.append(which_idnum) 

643 if verbose: 

644 log.debug( 

645 "find_critical_single_numerical_id(): " 

646 "possible_critical_idnums = {}", 

647 possible_critical_idnums) 

648 if len(possible_critical_idnums) == 1: 

649 return possible_critical_idnums[0] 

650 return None 

651 

652 def is_idnum_mandatory_in_policy( 

653 self, 

654 which_idnum: int, 

655 valid_idnums: List[int], 

656 verbose: bool = False) -> bool: 

657 """ 

658 Is the ID number mandatory in the specified policy? 

659 

660 Args: 

661 which_idnum: ID number type to test 

662 valid_idnums: ID number types that are valid on the server 

663 verbose: report reasons to debug log 

664 """ 

665 if which_idnum is None or which_idnum < 1: 

666 if verbose: 

667 log.debug("is_idnum_mandatory_in_policy(): bad ID type") 

668 return False 

669 if not self.contains_specific_idnum(which_idnum): 

670 if verbose: 

671 log.debug("is_idnum_mandatory_in_policy(): policy does not " 

672 "contain ID {}, so not mandatory", which_idnum) 

673 return False 

674 self.set_valid_idnums(valid_idnums) 

675 if not self.is_valid(): 

676 if verbose: 

677 log.debug("is_idnum_mandatory_in_policy(): policy invalid") 

678 return False 

679 

680 pip_with = PatientInfoPresence.make_uncaring_except(which_idnum, 

681 Q_TRUE) 

682 satisfies_with = self._value_for_pip(pip_with) 

683 if satisfies_with != Q_TRUE: 

684 if verbose: 

685 log.debug("is_idnum_mandatory_in_policy(): policy not " 

686 "satisfied by presence of ID {}, so not mandatory", 

687 which_idnum) 

688 return False 

689 pip_without = PatientInfoPresence.make_uncaring_except(which_idnum, 

690 Q_FALSE) 

691 satisfies_without = self._value_for_pip(pip_without) 

692 if satisfies_without != Q_FALSE: 

693 if verbose: 

694 log.debug("is_idnum_mandatory_in_policy(): policy satisfied " 

695 "without presence of ID {}, so not mandatory", 

696 which_idnum) 

697 return False 

698 # Thus, if we get here, the policy is unhappy with the absence of our 

699 # ID number type, but happy with it; therefore it is mandatory. 

700 if verbose: 

701 log.debug("is_idnum_mandatory_in_policy(): ID {} is mandatory", 

702 which_idnum) 

703 return True 

704 

705 def _requires_prohibits(self, token: int, 

706 verbose: bool = False) -> Tuple[bool, bool]: 

707 """ 

708 Does this policy require, and/or prohibit, a particular token? 

709 

710 Args: 

711 token: token to check 

712 verbose: report reasons to debug log 

713 

714 Returns: 

715 tuple: requires, prohibits 

716 """ 

717 pip_with = PatientInfoPresence.make_uncaring_except(token, Q_TRUE) 

718 satisfies_with = self._value_for_pip(pip_with) 

719 pip_without = PatientInfoPresence.make_uncaring_except(token, Q_FALSE) 

720 satisfies_without = self._value_for_pip(pip_without) 

721 requires = ( 

722 satisfies_with is Q_TRUE and 

723 satisfies_without is Q_FALSE 

724 ) 

725 prohibits = ( 

726 satisfies_with is Q_FALSE and 

727 satisfies_without is Q_TRUE 

728 ) 

729 if verbose: 

730 log.debug( 

731 "_requires_prohibits({t}): " 

732 "satisfies_with={sw}, " 

733 "satisfies_without={swo}, " 

734 "requires={r}, " 

735 "prohibits={p}", 

736 t=token_to_str(token), 

737 sw=satisfies_with, 

738 swo=satisfies_without, 

739 r=requires, 

740 p=prohibits, 

741 ) 

742 return requires, prohibits 

743 

744 def _requires_sex(self, verbose: bool = False) -> bool: 

745 """ 

746 Does this policy require sex to be present? 

747 

748 Args: 

749 verbose: report reasons to debug log 

750 """ 

751 requires, _ = self._requires_prohibits(TK_SEX, verbose=verbose) 

752 return requires 

753 

754 def _requires_an_idnum(self, verbose: bool = False) -> bool: 

755 """ 

756 Does this policy require an ID number to be present? 

757 

758 Args: 

759 verbose: report reasons to debug log 

760 """ 

761 if verbose: 

762 log.debug("_requires_an_idnum():") 

763 for token in self.specifically_mentioned_idnums() + [TK_ANY_IDNUM, 

764 TK_OTHER_IDNUM]: 

765 requires, _ = self._requires_prohibits(token, verbose=verbose) 

766 if requires: 

767 if verbose: 

768 log.debug("... requires ID number '{}'", 

769 token_to_str(token)) 

770 return True 

771 return False 

772 

773 # def _less_restrictive_than(self, other: "TokenizedPolicy", 

774 # valid_idnums: List[int], 

775 # verbose: bool = False) -> bool: 

776 # """ 

777 # Is this ("self") policy less restrictive than the "other" policy? 

778 # 

779 # "More restrictive" means "requires more information". 

780 # "Less restrictive" means "requires or enforces less information". 

781 # 

782 # Therefore, we must return True if we can find a situation where "self" 

783 # is satisfied but "other" is not. 

784 # 

785 # Args: 

786 # other: the other policy 

787 # valid_idnums: ID number types that are valid on the server 

788 # verbose: report reasons to debug log 

789 # 

790 # This is very difficult. Abandoned this generic attempt in favour of a 

791 # specific hard-coded check for the tablet policy. 

792 # """ 

793 # if verbose: 

794 # log.debug("_less_restrictive_than(): self={}, other={}", 

795 # self, other) 

796 # possible_tokens = valid_idnums + NON_IDNUM_INFO_TOKENS 

797 # for token in possible_tokens: 

798 # # Self 

799 # self_requires, self_prohibits = self._requires_prohibits( 

800 # token, valid_idnums) 

801 # # Other 

802 # pip_with = PatientInfoPresence.make_uncaring_except( 

803 # token, Q_TRUE, valid_idnums) 

804 # other_satisfies_with = other._value_for_pip(pip_with) 

805 # pip_without = PatientInfoPresence.make_uncaring_except( 

806 # token, Q_FALSE, valid_idnums) 

807 # other_satisfies_without_1 = other._value_for_pip(pip_without) 

808 # pip_without.special_anyidnum_present = Q_TRUE 

809 # other_satisfies_without_2 = other._value_for_pip(pip_without) 

810 # other_requires = ( 

811 # other_satisfies_with is Q_TRUE and 

812 # other_satisfies_without_1 is Q_FALSE and 

813 # other_satisfies_without_2 is Q_FALSE 

814 # ) 

815 # other_prohibits = ( 

816 # other_satisfies_with is Q_FALSE and 

817 # other_satisfies_without_1 is Q_TRUE and 

818 # other_satisfies_without_2 is Q_TRUE 

819 # ) 

820 # if verbose: 

821 # log.debug( 

822 # "... for {t}: " 

823 # "self_requires={sr}, " 

824 # "self_prohibits={sp}, " 

825 # "other_satisfies_with={osw}, " 

826 # "other_satisfies_without_1={oswo1}, " 

827 # "other_satisfies_without_2={oswo2}, " 

828 # "other_requires={or_}", 

829 # "other_prohibits={op}", 

830 # t=token_to_str(token), 

831 # sr=self_requires, 

832 # sp=self_prohibits, 

833 # osw=other_satisfies_with, 

834 # oswo1=other_satisfies_without_1, 

835 # oswo2=other_satisfies_without_2, 

836 # or_=other_requires, 

837 # op=other_prohibits, 

838 # ) 

839 # 

840 # if other_requires and not self_requires: 

841 # # The "self" policy is LESS RESTRICTIVE (requires less info). 

842 # if verbose: 

843 # log.debug( 

844 # "... self does not require ID type {}, but other does " # noqa 

845 # "require it; therefore self is less restrictive", 

846 # token) 

847 # return True 

848 # # if self_prohibits and not other_prohibits: 

849 # # # The "self" policy is LESS RESTRICTIVE (enforces less info). # noqa 

850 # # if verbose: 

851 # # log.debug( 

852 # # "... self prohibits ID type {}, but other does not " # noqa 

853 # # "prohibit it; therefore self is less restrictive", 

854 # # token) 

855 # # return True 

856 # if verbose: 

857 # log.debug( 

858 # "... by elimination, self [{}] not less " 

859 # "restrictive than other [{}]", 

860 # self, other 

861 # ) 

862 # return False 

863 

864 def _compatible_with_tablet_id_policy(self, 

865 verbose: bool = False) -> bool: 

866 """ 

867 Is this policy compatible with :data:`TABLET_ID_POLICY`? 

868 

869 The "self" policy may be MORE restrictive than the tablet minimum ID 

870 policy, but may not be LESS restrictive. 

871 

872 Args: 

873 verbose: report reasons to debug log 

874 

875 Internal function -- doesn't used cached information. 

876 """ 

877 # Method 1: abandoned. 

878 # We previously used a version of _less_restrictive_than() that 

879 # did a brute-force attempt, but that became prohibitive as ID numbers 

880 # got added. 

881 # A generic method is very hard (see above) -- not properly succeeded 

882 # yet. 

883 # 

884 # return not self._less_restrictive_than( 

885 # TABLET_ID_POLICY, valid_idnums, verbose=verbose) 

886 

887 # Method 2: manual. 

888 if verbose: 

889 log.debug("_compatible_with_tablet_id_policy():") 

890 requires_sex = self._requires_sex(verbose=verbose) 

891 if requires_sex: 

892 if verbose: 

893 log.debug("... requires sex") 

894 else: 

895 if verbose: 

896 log.debug("... doesn't require sex; returning False") 

897 return False 

898 requires_an_idnum = self._requires_an_idnum(verbose=verbose) 

899 if requires_an_idnum: 

900 if verbose: 

901 log.debug("... requires an ID number; returning True") 

902 return True 

903 if verbose: 

904 log.debug("... does not require an ID number; trying alternatives") 

905 other_mandatory = [TK_FORENAME, TK_SURNAME, TK_DOB, TK_EMAIL] 

906 for token in other_mandatory: 

907 requires, _ = self._requires_prohibits(token, verbose=verbose) 

908 if not requires: 

909 if verbose: 

910 log.debug("... does not require '{}'; returning False", 

911 token_to_str(token)) 

912 return False 

913 log.debug("... requires all of {!r}; returning True", 

914 [token_to_str(t) for t in other_mandatory]) 

915 return True 

916 

917 def compatible_with_tablet_id_policy(self, 

918 valid_idnums: List[int], 

919 verbose: bool = False) -> bool: 

920 """ 

921 Is this policy compatible with :data:`TABLET_ID_POLICY`? 

922 

923 The "self" policy may be MORE restrictive than the tablet minimum ID 

924 policy, but may not be LESS restrictive. 

925 

926 Args: 

927 valid_idnums: ID number types that are valid on the server 

928 verbose: report reasons to debug log 

929 """ 

930 self.set_valid_idnums(valid_idnums) 

931 if not self.is_valid(verbose=verbose): 

932 return False 

933 return self._compatible_with_tablet_id_policy(verbose=verbose) 

934 

935 # ------------------------------------------------------------------------- 

936 # Check if a patient satisfies the policy 

937 # ------------------------------------------------------------------------- 

938 

939 def _value_for_ptinfo(self, ptinfo: BarePatientInfo) -> QuadState: 

940 """ 

941 What does the policy evaluate to for a given patient info object? 

942 

943 Args: 

944 ptinfo: 

945 a `camcops_server.cc_modules.cc_simpleobjects.BarePatientInfo` 

946 

947 Returns: 

948 a :class:`QuadState` quad-state value 

949 """ 

950 pip = PatientInfoPresence.make_from_ptinfo( 

951 ptinfo, 

952 self.specifically_mentioned_idnums() 

953 ) 

954 return self._value_for_pip(pip) 

955 

956 def _value_for_pip(self, pip: PatientInfoPresence) -> QuadState: 

957 """ 

958 What does the policy evaluate to for a given patient info presence 

959 object? 

960 

961 Args: 

962 pip: 

963 a `camcops_server.cc_modules.cc_simpleobjects.PatientInfoPresence` 

964 

965 Returns: 

966 a :class:`QuadState` quad-state value 

967 """ # noqa 

968 def content_token_processor(token: int) -> QuadState: 

969 return self._element_value_test_pip(pip, token) 

970 

971 return self._chunk_value( 

972 self.tokens, 

973 content_token_processor=content_token_processor) 

974 # ... which is recursive 

975 

976 def satisfies_id_policy(self, ptinfo: BarePatientInfo) -> bool: 

977 """ 

978 Does the patient information in ptinfo satisfy the specified ID policy? 

979 

980 Args: 

981 ptinfo: 

982 a `camcops_server.cc_modules.cc_simpleobjects.BarePatientInfo` 

983 """ 

984 return self._value_for_ptinfo(ptinfo) is Q_TRUE 

985 

986 # ------------------------------------------------------------------------- 

987 # Functions for the policy to parse itself and compare itself to a patient 

988 # ------------------------------------------------------------------------- 

989 

990 def _chunk_value(self, 

991 tokens: TOKENIZED_POLICY_TYPE, 

992 content_token_processor: CONTENT_TOKEN_PROCESSOR_TYPE) \ 

993 -> QuadState: 

994 """ 

995 Applies the policy to the patient info in ``ptinfo``. 

996 Can be used recursively. 

997 

998 Args: 

999 tokens: 

1000 a tokenized policy 

1001 content_token_processor: 

1002 a function to be called for each "content" token, which returns 

1003 its Boolean value, or ``None`` in case of failure 

1004 

1005 Returns: 

1006 a :class:`QuadState` quad-state value 

1007 """ 

1008 want_content = True 

1009 processing_and = False 

1010 processing_or = False 

1011 index = 0 

1012 value = None # type: Optional[QuadState] 

1013 while index < len(tokens): 

1014 if want_content: 

1015 nextchunk, index = self._content_chunk_value( 

1016 tokens, index, content_token_processor) 

1017 if nextchunk is Q_ERROR: 

1018 return Q_ERROR # fail 

1019 if value is None: 

1020 value = nextchunk 

1021 elif processing_and: 

1022 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1023 # implement logical AND 

1024 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1025 value = quad_and(value, nextchunk) 

1026 elif processing_or: 

1027 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1028 # implement logical OR 

1029 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1030 value = quad_or(value, nextchunk) 

1031 else: 

1032 # Error; shouldn't get here 

1033 return Q_ERROR 

1034 processing_and = False 

1035 processing_or = False 

1036 else: 

1037 # Want operator 

1038 operator, index = self._op(tokens, index) 

1039 if operator is None: 

1040 return Q_ERROR # fail 

1041 if operator == TK_AND: 

1042 processing_and = True 

1043 elif operator == TK_OR: 

1044 processing_or = True 

1045 else: 

1046 # Error; shouldn't get here 

1047 return Q_ERROR 

1048 want_content = not want_content 

1049 if want_content: 

1050 log.debug("_chunk_value(): ended wanting content; bad policy") 

1051 return Q_ERROR 

1052 return value 

1053 

1054 def _content_chunk_value( 

1055 self, 

1056 tokens: TOKENIZED_POLICY_TYPE, 

1057 start: int, 

1058 content_token_processor: CONTENT_TOKEN_PROCESSOR_TYPE) \ 

1059 -> Tuple[QuadState, int]: 

1060 """ 

1061 Applies part of a policy to ``ptinfo``. The part of policy pointed to 

1062 by ``start`` represents something -- "content" -- that should return a 

1063 value (not an operator, for example). Called by :func:`id_policy_chunk` 

1064 (q.v.). 

1065 

1066 Args: 

1067 tokens: 

1068 a tokenized policy (list of integers) 

1069 start: 

1070 zero-based index of the first token to check 

1071 content_token_processor: 

1072 a function to be called for each "content" token, which returns 

1073 its Boolean value, or ``None`` in case of failure 

1074 

1075 Returns: 

1076 tuple: chunk_value, next_index. ``chunk_value`` is ``True`` if the 

1077 specified chunk is satisfied by the ``ptinfo``, ``False`` if it 

1078 isn't, and ``None`` if there was an error. ``next_index`` is the 

1079 index of the next token after this chunk. 

1080 

1081 """ 

1082 if start >= len(tokens): 

1083 log.debug("_content_chunk_value(): " 

1084 "beyond end of policy; bad policy") 

1085 return Q_ERROR, start 

1086 token = tokens[start] 

1087 if token in [TK_RPAREN, TK_AND, TK_OR]: 

1088 log.debug("_content_chunk_value(): " 

1089 "chunk starts with ), AND, or OR; bad policy") 

1090 return Q_ERROR, start 

1091 elif token == TK_LPAREN: 

1092 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1093 # implement parentheses 

1094 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1095 subchunkstart = start + 1 # exclude the opening bracket 

1096 # Find closing parenthesis 

1097 depth = 1 

1098 searchidx = subchunkstart 

1099 while depth > 0: 

1100 if searchidx >= len(tokens): 

1101 log.debug("_content_chunk_value(): " 

1102 "Unmatched left parenthesis; bad policy") 

1103 return Q_ERROR, start 

1104 elif tokens[searchidx] == TK_LPAREN: 

1105 depth += 1 

1106 elif tokens[searchidx] == TK_RPAREN: 

1107 depth -= 1 

1108 searchidx += 1 

1109 subchunkend = searchidx - 1 

1110 # ... to exclude the closing bracket from the analysed subchunk 

1111 chunk_value = self._chunk_value( 

1112 tokens[subchunkstart:subchunkend], content_token_processor) 

1113 return chunk_value, subchunkend + 1 # to move past the closing bracket # noqa 

1114 elif token == TK_NOT: 

1115 next_value, next_index = self._content_chunk_value( 

1116 tokens, start + 1, content_token_processor) 

1117 if next_value is Q_ERROR: 

1118 return next_value, start 

1119 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1120 # implement logical NOT 

1121 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

1122 return quad_not(next_value), next_index 

1123 else: 

1124 # meaningful token 

1125 return content_token_processor(token), start + 1 

1126 

1127 @classmethod 

1128 def _op(cls, policy: TOKENIZED_POLICY_TYPE, start: int) \ 

1129 -> Tuple[Optional[TOKEN_TYPE], int]: 

1130 """ 

1131 Returns an operator from the policy, beginning at index ``start``, or 

1132 ``None`` if there wasn't an operator there. 

1133 

1134 policy: 

1135 a tokenized policy (list of integers) 

1136 start: 

1137 zero-based index of the first token to check 

1138 

1139 Returns: 

1140 tuple: ``operator, next_index``. ``operator`` is the operator's 

1141 integer token or ``None``. ``next_index`` gives the next index of 

1142 the policy to check at. 

1143 """ 

1144 if start >= len(policy): 

1145 log.debug("_op(): beyond end of policy") 

1146 return None, start 

1147 token = policy[start] 

1148 if token in [TK_AND, TK_OR]: 

1149 return token, start + 1 

1150 else: 

1151 log.debug("_op(): not an operator; bad policy") 

1152 # Not an operator 

1153 return None, start 

1154 

1155 # Things to do with content tokens 1: are they present in patient info? 

1156 

1157 @staticmethod 

1158 def _element_value_test_pip(pip: PatientInfoPresence, 

1159 token: TOKEN_TYPE) -> QuadState: 

1160 """ 

1161 Returns the "value" of a content token as judged against the patient 

1162 information. For example, if the patient information contains a date of 

1163 birth, a ``TK_DOB`` token will evaluate to ``True``. 

1164 

1165 Args: 

1166 pip: 

1167 a `camcops_server.cc_modules.cc_simpleobjects.PatientInfoPresence` 

1168 token: 

1169 an integer token from the policy 

1170 

1171 Returns: 

1172 a :class:`QuadState` quad-state value 

1173 """ # noqa 

1174 assert is_info_token(token) 

1175 if token == TK_ANY_IDNUM: 

1176 return pip.any_idnum_present() 

1177 else: 

1178 return pip.is_present(token) 

1179 

1180 

1181# ============================================================================= 

1182# Tablet ID policy 

1183# ============================================================================= 

1184 

1185TABLET_ID_POLICY_STR = "sex AND ((forename AND surname AND dob) OR anyidnum)" 

1186TABLET_ID_POLICY = TokenizedPolicy(TABLET_ID_POLICY_STR)