Coverage for cc_modules/cc_policy.py : 29%

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
3"""
4camcops_server/cc_modules/cc_policy.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
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.
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.
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/>.
25===============================================================================
27**Represents ID number policies.**
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!)
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).
40"""
42import io
43import logging
44import tokenize
45from typing import Callable, Dict, List, Optional, Tuple
47from cardinal_pythonlib.dicts import reversedict
48from cardinal_pythonlib.logs import BraceStyleAdapter
49from cardinal_pythonlib.reprfunc import auto_repr
51from camcops_server.cc_modules.cc_simpleobjects import BarePatientInfo
53log = BraceStyleAdapter(logging.getLogger(__name__))
56# =============================================================================
57# Tokens
58# =============================================================================
60TOKEN_TYPE = int
61TOKENIZED_POLICY_TYPE = List[TOKEN_TYPE]
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
81# Tokens for ID numbers are from 1 upwards.
83POLICY_TOKEN_DICT = {
84 "(": TK_LPAREN,
85 ")": TK_RPAREN,
86 "AND": TK_AND,
87 "OR": TK_OR,
88 "NOT": TK_NOT,
90 "ANYIDNUM": TK_ANY_IDNUM,
91 "OTHERIDNUM": TK_OTHER_IDNUM,
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)
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]
110TOKEN_IDNUM_PREFIX = "IDNUM"
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
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)
131# =============================================================================
132# Quad-state logic
133# =============================================================================
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"
147Q_TRUE = QuadState()
148Q_FALSE = QuadState()
149Q_ERROR = QuadState()
150Q_DONT_CARE = QuadState()
153def bool_to_quad(x: bool) -> QuadState:
154 return Q_TRUE if x else Q_FALSE
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
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
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
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
210DEBUG_QUAD_STATE_LOGIC = False
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")
218# =============================================================================
219# PatientInfoPresence
220# =============================================================================
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)
240 def __repr__(self) -> str:
241 return auto_repr(self)
243 def is_present(self, token: int,
244 default: QuadState = None) -> QuadState:
245 """
246 Is information represented by a particular token present?
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.
253 Returns:
254 a :class:`QuadState`
255 """
256 return self.present.get(token, default or self.default)
258 @property
259 def forename_present(self) -> QuadState:
260 return self.is_present(TK_FORENAME)
262 @property
263 def surname_present(self) -> QuadState:
264 return self.is_present(TK_SURNAME)
266 @property
267 def sex_present(self) -> QuadState:
268 return self.is_present(TK_SEX)
270 @property
271 def dob_present(self) -> QuadState:
272 return self.is_present(TK_DOB)
274 @property
275 def address_present(self) -> QuadState:
276 return self.is_present(TK_ADDRESS)
278 @property
279 def email_present(self) -> QuadState:
280 return self.is_present(TK_EMAIL)
282 @property
283 def gp_present(self) -> QuadState:
284 return self.is_present(TK_GP)
286 @property
287 def otherdetails_present(self) -> QuadState:
288 return self.is_present(TK_OTHER_DETAILS)
290 @property
291 def otheridnum_present(self) -> QuadState:
292 return self.is_present(TK_OTHER_IDNUM)
294 @property
295 def special_anyidnum_present(self) -> QuadState:
296 return self.is_present(TK_ANY_IDNUM)
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)
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
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)
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)
348 def set_idnum_presence(self, which_idnum: int, present: QuadState) -> None:
349 """
350 Set the "presence" state for one ID number type.
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
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
371# =============================================================================
372# More constants
373# =============================================================================
375CONTENT_TOKEN_PROCESSOR_TYPE = Callable[[int], QuadState]
378# =============================================================================
379# TokenizedPolicy
380# =============================================================================
382class TokenizedPolicy(object):
383 """
384 Represents a tokenized ID policy.
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]
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
403 # -------------------------------------------------------------------------
404 # ID number info
405 # -------------------------------------------------------------------------
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.
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
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 )
429 # -------------------------------------------------------------------------
430 # Tokenize
431 # -------------------------------------------------------------------------
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
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
478 # -------------------------------------------------------------------------
479 # Validity checks
480 # -------------------------------------------------------------------------
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
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?
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
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?
522 Checks the following:
524 - valid syntax
525 - refers only to ID number types defined on the server
526 - is compatible with the tablet ID policy
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
551 # -------------------------------------------------------------------------
552 # Information about the ID number types the policy refers to
553 # -------------------------------------------------------------------------
555 def relevant_idnums(self, valid_idnums: List[int]) -> List[int]:
556 """
557 Which ID numbers are relevant to this policy?
559 Args:
560 valid_idnums: ID number types that are valid on the server
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
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]
585 def contains_specific_idnum(self, which_idnum: int) -> bool:
586 """
587 Does the policy refer specifically to the given ID number type?
589 Args:
590 which_idnum: ID number type to test
591 """
592 assert which_idnum > 0
593 return which_idnum in self.tokens
595 # -------------------------------------------------------------------------
596 # More complex attributes
597 # -------------------------------------------------------------------------
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.
607 Args:
608 valid_idnums: ID number types that are valid on the server
609 verbose: report reasons to debug log
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
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?
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
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
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?
710 Args:
711 token: token to check
712 verbose: report reasons to debug log
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
744 def _requires_sex(self, verbose: bool = False) -> bool:
745 """
746 Does this policy require sex to be present?
748 Args:
749 verbose: report reasons to debug log
750 """
751 requires, _ = self._requires_prohibits(TK_SEX, verbose=verbose)
752 return requires
754 def _requires_an_idnum(self, verbose: bool = False) -> bool:
755 """
756 Does this policy require an ID number to be present?
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
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
864 def _compatible_with_tablet_id_policy(self,
865 verbose: bool = False) -> bool:
866 """
867 Is this policy compatible with :data:`TABLET_ID_POLICY`?
869 The "self" policy may be MORE restrictive than the tablet minimum ID
870 policy, but may not be LESS restrictive.
872 Args:
873 verbose: report reasons to debug log
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)
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
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`?
923 The "self" policy may be MORE restrictive than the tablet minimum ID
924 policy, but may not be LESS restrictive.
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)
935 # -------------------------------------------------------------------------
936 # Check if a patient satisfies the policy
937 # -------------------------------------------------------------------------
939 def _value_for_ptinfo(self, ptinfo: BarePatientInfo) -> QuadState:
940 """
941 What does the policy evaluate to for a given patient info object?
943 Args:
944 ptinfo:
945 a `camcops_server.cc_modules.cc_simpleobjects.BarePatientInfo`
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)
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?
961 Args:
962 pip:
963 a `camcops_server.cc_modules.cc_simpleobjects.PatientInfoPresence`
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)
971 return self._chunk_value(
972 self.tokens,
973 content_token_processor=content_token_processor)
974 # ... which is recursive
976 def satisfies_id_policy(self, ptinfo: BarePatientInfo) -> bool:
977 """
978 Does the patient information in ptinfo satisfy the specified ID policy?
980 Args:
981 ptinfo:
982 a `camcops_server.cc_modules.cc_simpleobjects.BarePatientInfo`
983 """
984 return self._value_for_ptinfo(ptinfo) is Q_TRUE
986 # -------------------------------------------------------------------------
987 # Functions for the policy to parse itself and compare itself to a patient
988 # -------------------------------------------------------------------------
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.
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
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
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.).
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
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.
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
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.
1134 policy:
1135 a tokenized policy (list of integers)
1136 start:
1137 zero-based index of the first token to check
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
1155 # Things to do with content tokens 1: are they present in patient info?
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``.
1165 Args:
1166 pip:
1167 a `camcops_server.cc_modules.cc_simpleobjects.PatientInfoPresence`
1168 token:
1169 an integer token from the policy
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)
1181# =============================================================================
1182# Tablet ID policy
1183# =============================================================================
1185TABLET_ID_POLICY_STR = "sex AND ((forename AND surname AND dob) OR anyidnum)"
1186TABLET_ID_POLICY = TokenizedPolicy(TABLET_ID_POLICY_STR)