Coverage for cc_modules/cc_policy.py: 29%
449 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_policy.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28**Represents ID number policies.**
30Note that the upload script should NOT attempt to verify patients against the
31ID policy, not least because tablets are allowed to upload task data (in a
32separate transaction) before uploading patients; referential integrity would be
33very hard to police. So the tablet software deals with ID compliance. (Also,
34the superuser can change the server's ID policy retrospectively!)
36Both the client and the server do policy tokenizing and can check patient info
37against policies. The server has additional code to answer questions like "is
38this policy valid?" (in general and in the context of the server's
39configuration).
41"""
43import io
44import logging
45import tokenize
46from typing import Callable, Dict, List, Optional, Tuple
48from cardinal_pythonlib.dicts import reversedict
49from cardinal_pythonlib.logs import BraceStyleAdapter
50from cardinal_pythonlib.reprfunc import auto_repr
52from camcops_server.cc_modules.cc_simpleobjects import BarePatientInfo
54log = BraceStyleAdapter(logging.getLogger(__name__))
57# =============================================================================
58# Tokens
59# =============================================================================
61TOKEN_TYPE = int
62TOKENIZED_POLICY_TYPE = List[TOKEN_TYPE]
64# http://stackoverflow.com/questions/36932
65BAD_TOKEN = 0
66TK_LPAREN = -1
67TK_RPAREN = -2
68TK_AND = -3
69TK_OR = -4
70TK_NOT = -5
71TK_ANY_IDNUM = -6
72TK_OTHER_IDNUM = -7
73TK_FORENAME = -8
74TK_SURNAME = -9
75TK_SEX = -10
76TK_DOB = -11
77TK_ADDRESS = -12
78TK_GP = -13
79TK_OTHER_DETAILS = -14
80TK_EMAIL = -15
82# Tokens for ID numbers are from 1 upwards.
84POLICY_TOKEN_DICT = {
85 "(": TK_LPAREN,
86 ")": TK_RPAREN,
87 "AND": TK_AND,
88 "OR": TK_OR,
89 "NOT": TK_NOT,
90 "ANYIDNUM": TK_ANY_IDNUM,
91 "OTHERIDNUM": TK_OTHER_IDNUM,
92 "FORENAME": TK_FORENAME,
93 "SURNAME": TK_SURNAME,
94 "SEX": TK_SEX,
95 "DOB": TK_DOB,
96 "ADDRESS": TK_ADDRESS,
97 "GP": TK_GP,
98 "OTHERDETAILS": TK_OTHER_DETAILS,
99 "EMAIL": TK_EMAIL,
100}
101TOKEN_POLICY_DICT = reversedict(POLICY_TOKEN_DICT)
103NON_IDNUM_INFO_TOKENS = [
104 TK_OTHER_IDNUM,
105 TK_ANY_IDNUM,
106 TK_FORENAME,
107 TK_SURNAME,
108 TK_SEX,
109 TK_DOB,
110 TK_ADDRESS,
111 TK_GP,
112 TK_OTHER_DETAILS,
113 TK_EMAIL,
114]
116TOKEN_IDNUM_PREFIX = "IDNUM"
119def is_info_token(token: int) -> bool:
120 """
121 Is the token a kind that represents information, not (for example) an
122 operator?
123 """
124 return token > 0 or token in NON_IDNUM_INFO_TOKENS
127def token_to_str(token: int) -> str:
128 """
129 Returns a string version of the specified token.
130 """
131 if token < 0:
132 return TOKEN_POLICY_DICT.get(token)
133 else:
134 return TOKEN_IDNUM_PREFIX + str(token)
137# =============================================================================
138# Quad-state logic
139# =============================================================================
142class QuadState(object):
143 def __str__(self) -> str:
144 if self is Q_TRUE:
145 return "QTrue"
146 elif self is Q_FALSE:
147 return "QFalse"
148 elif self is Q_DONT_CARE:
149 return "QDontCare"
150 else:
151 return "QError"
154Q_TRUE = QuadState()
155Q_FALSE = QuadState()
156Q_ERROR = QuadState()
157Q_DONT_CARE = QuadState()
160def bool_to_quad(x: bool) -> QuadState:
161 return Q_TRUE if x else Q_FALSE
164def quad_not(x: QuadState) -> QuadState:
165 # Boolean logic
166 if x is Q_TRUE:
167 return Q_FALSE
168 elif x is Q_FALSE:
169 return Q_TRUE
170 # Unusual logic
171 elif x is Q_DONT_CARE:
172 return Q_DONT_CARE
173 else:
174 return Q_ERROR
177def quad_and(x: QuadState, y: QuadState) -> QuadState:
178 either = (x, y)
179 # Unusual logic
180 if Q_ERROR in either:
181 return Q_ERROR
182 elif Q_DONT_CARE in either:
183 other = either[1] if either[0] == Q_DONT_CARE else either[0]
184 return other
185 # Boolean logic
186 elif x is Q_TRUE and y is Q_TRUE:
187 return Q_TRUE
188 else:
189 return Q_FALSE
192def quad_or(x: QuadState, y: QuadState) -> QuadState:
193 either = (x, y)
194 # Unusual logic
195 if Q_ERROR in either:
196 return Q_ERROR
197 elif Q_DONT_CARE in either:
198 other = either[1] if either[0] == Q_DONT_CARE else either[0]
199 return other
200 # Boolean logic
201 elif x is Q_TRUE or y is Q_TRUE:
202 return Q_TRUE
203 else:
204 return Q_FALSE
207def debug_wrapper(fn: Callable, name: str) -> Callable:
208 def wrap(*args, **kwargs) -> QuadState:
209 result = fn(*args, **kwargs)
210 arglist = [str(x) for x in args] + [
211 f"{k}={v}" for k, v in kwargs.items()
212 ]
213 log.critical("{}({}) -> {}".format(name, ", ".join(arglist), result))
214 return result
216 return wrap
219DEBUG_QUAD_STATE_LOGIC = False
221if DEBUG_QUAD_STATE_LOGIC:
222 quad_not = debug_wrapper(quad_not, "quad_not")
223 quad_and = debug_wrapper(quad_and, "quad_and")
224 quad_or = debug_wrapper(quad_or, "quad_or")
227# =============================================================================
228# PatientInfoPresence
229# =============================================================================
232class PatientInfoPresence(object):
233 """
234 Represents simply the presence/absence of different kinds of information
235 about a patient.
236 """
238 def __init__(
239 self,
240 present: Dict[int, QuadState] = None,
241 default: QuadState = Q_FALSE,
242 ) -> None:
243 """
244 Args:
245 present: map from token to :class:`QuadState`
246 default: default :class:`QuadState` to return if unspecified
247 """
248 self.present = present or {} # type: Dict[int, QuadState]
249 self.default = default
250 for t in self.present.keys():
251 assert is_info_token(t)
253 def __repr__(self) -> str:
254 return auto_repr(self)
256 def is_present(self, token: int, default: QuadState = None) -> QuadState:
257 """
258 Is information represented by a particular token present?
260 Args:
261 token: token to check for; e.g. :data:`TK_FORENAME`
262 default: default :class:`QuadState` to return if unspecified; if
263 this is None, ``self.default`` is used.
265 Returns:
266 a :class:`QuadState`
267 """
268 return self.present.get(token, default or self.default)
270 @property
271 def forename_present(self) -> QuadState:
272 return self.is_present(TK_FORENAME)
274 @property
275 def surname_present(self) -> QuadState:
276 return self.is_present(TK_SURNAME)
278 @property
279 def sex_present(self) -> QuadState:
280 return self.is_present(TK_SEX)
282 @property
283 def dob_present(self) -> QuadState:
284 return self.is_present(TK_DOB)
286 @property
287 def address_present(self) -> QuadState:
288 return self.is_present(TK_ADDRESS)
290 @property
291 def email_present(self) -> QuadState:
292 return self.is_present(TK_EMAIL)
294 @property
295 def gp_present(self) -> QuadState:
296 return self.is_present(TK_GP)
298 @property
299 def otherdetails_present(self) -> QuadState:
300 return self.is_present(TK_OTHER_DETAILS)
302 @property
303 def otheridnum_present(self) -> QuadState:
304 return self.is_present(TK_OTHER_IDNUM)
306 @property
307 def special_anyidnum_present(self) -> QuadState:
308 return self.is_present(TK_ANY_IDNUM)
310 def idnum_present(self, which_idnum: int) -> QuadState:
311 """
312 Is the specified ID number type present?
313 """
314 assert which_idnum > 0
315 return self.is_present(which_idnum)
317 def any_idnum_present(self) -> QuadState:
318 """
319 Is at least one ID number present?
320 """
321 for k, v in self.present.items():
322 if k > 0 and v is Q_TRUE:
323 return Q_TRUE
324 return self.special_anyidnum_present
326 @classmethod
327 def make_from_ptinfo(
328 cls, ptinfo: BarePatientInfo, policy_mentioned_idnums: List[int]
329 ) -> "PatientInfoPresence":
330 """
331 Returns a :class:`PatientInfoPresence` representing whether different
332 kinds of information about the patient are present or not.
333 """
334 presences = {
335 TK_FORENAME: bool_to_quad(bool(ptinfo.forename)),
336 TK_SURNAME: bool_to_quad(bool(ptinfo.surname)),
337 TK_SEX: bool_to_quad(bool(ptinfo.sex)),
338 TK_DOB: bool_to_quad(ptinfo.dob is not None),
339 TK_ADDRESS: bool_to_quad(bool(ptinfo.address)),
340 TK_EMAIL: bool_to_quad(bool(ptinfo.email)),
341 TK_GP: bool_to_quad(bool(ptinfo.gp)),
342 TK_OTHER_DETAILS: bool_to_quad(bool(ptinfo.otherdetails)),
343 TK_OTHER_IDNUM: Q_FALSE, # may change
344 } # type: Dict[int, QuadState]
345 for iddef in ptinfo.idnum_definitions:
346 this_idnum_present = iddef.idnum_value is not None
347 presences[iddef.which_idnum] = bool_to_quad(this_idnum_present)
348 if iddef.which_idnum not in policy_mentioned_idnums:
349 presences[TK_OTHER_IDNUM] = Q_TRUE
350 return cls(presences, default=Q_FALSE)
352 @classmethod
353 def make_uncaring(cls) -> "PatientInfoPresence":
354 """
355 Makes a :class:`PatientInfoPresence` that doesn't care about anything.
356 """
357 return cls({}, default=Q_DONT_CARE)
359 def set_idnum_presence(self, which_idnum: int, present: QuadState) -> None:
360 """
361 Set the "presence" state for one ID number type.
363 Args:
364 which_idnum: which ID number type
365 present: its state of being present (or not, or other states)
366 """
367 self.present[which_idnum] = present
369 @classmethod
370 def make_uncaring_except(
371 cls, token: int, present: QuadState
372 ) -> "PatientInfoPresence":
373 """
374 Make a :class:`PatientInfoPresence` that is uncaring except for one
375 thing, specified by token.
376 """
377 assert is_info_token(token)
378 pip = cls.make_uncaring()
379 pip.present[token] = present
380 return pip
383# =============================================================================
384# More constants
385# =============================================================================
387CONTENT_TOKEN_PROCESSOR_TYPE = Callable[[int], QuadState]
390# =============================================================================
391# TokenizedPolicy
392# =============================================================================
395class TokenizedPolicy(object):
396 """
397 Represents a tokenized ID policy.
399 A tokenized policy is a policy represented by a sequence of integers;
400 0 means "bad token"; negative numbers represent fixed things like
401 "forename" or "left parenthesis" or "and"; positive numbers represent
402 ID number types.
403 """
405 def __init__(self, policy: str) -> None:
406 self.tokens = self.get_tokenized_id_policy(policy)
407 self._syntactically_valid = None # type: Optional[bool]
408 self.valid_idnums = None # type: Optional[List[int]]
409 self._valid_for_idnums = None # type: Optional[bool]
411 def __str__(self) -> str:
412 policy = " ".join(token_to_str(t) for t in self.tokens)
413 policy = policy.replace("( ", "(")
414 policy = policy.replace(" )", ")")
415 return policy
417 # -------------------------------------------------------------------------
418 # ID number info
419 # -------------------------------------------------------------------------
421 def set_valid_idnums(self, valid_idnums: List[int]) -> None:
422 """
423 Make a note of which ID number types are currently valid.
424 Caches "valid for these ID numbers" information.
426 Args:
427 valid_idnums: list of valid ID number types
428 """
429 sorted_idnums = sorted(valid_idnums)
430 if sorted_idnums != self.valid_idnums:
431 self.valid_idnums = sorted_idnums
432 self._valid_for_idnums = None # clear cache
434 def require_valid_idnum_info(self) -> None:
435 """
436 Checks that set_valid_idnums() has been called properly, or raises
437 :exc:`AssertionError`.
438 """
439 assert (
440 self.valid_idnums is not None
441 ), "Must call set_valid_idnums() first! Currently: {!r}"
443 # -------------------------------------------------------------------------
444 # Tokenize
445 # -------------------------------------------------------------------------
447 @staticmethod
448 def name_to_token(name: str) -> int:
449 """
450 Converts an upper-case string token name (such as ``DOB``) to an
451 integer token.
452 """
453 if name in POLICY_TOKEN_DICT:
454 return POLICY_TOKEN_DICT[name]
455 if name.startswith(TOKEN_IDNUM_PREFIX):
456 nstr = name[len(TOKEN_IDNUM_PREFIX) :] # noqa: E203
457 try:
458 return int(nstr)
459 except (TypeError, ValueError):
460 return BAD_TOKEN
461 return BAD_TOKEN
463 @classmethod
464 def get_tokenized_id_policy(cls, policy: str) -> TOKENIZED_POLICY_TYPE:
465 """
466 Takes a string policy and returns a tokenized policy, meaning a list of
467 integer tokens, or ``[]``.
468 """
469 if policy is None:
470 return []
471 # http://stackoverflow.com/questions/88613
472 string_index = 1
473 # single line, upper case:
474 policy = " ".join(policy.strip().upper().splitlines())
475 try:
476 tokenstrings = list(
477 token[string_index]
478 for token in tokenize.generate_tokens(
479 io.StringIO(policy).readline
480 )
481 if token[string_index]
482 )
483 except tokenize.TokenError:
484 # something went wrong
485 return []
486 tokens = [cls.name_to_token(k) for k in tokenstrings]
487 if any(t == BAD_TOKEN for t in tokens):
488 # There's something bad in there.
489 return []
490 return tokens
492 # -------------------------------------------------------------------------
493 # Validity checks
494 # -------------------------------------------------------------------------
496 def is_syntactically_valid(self) -> bool:
497 """
498 Is the policy syntactically valid? This is a basic check.
499 """
500 if self._syntactically_valid is None:
501 # Cache it
502 if not self.tokens:
503 self._syntactically_valid = False
504 else:
505 # Evaluate against a dummy patient info object. If we get None,
506 # it's gone wrong.
507 pip = PatientInfoPresence()
508 value = self._value_for_pip(pip)
509 self._syntactically_valid = value is not Q_ERROR
510 return self._syntactically_valid
512 def is_valid(
513 self, valid_idnums: List[int] = None, verbose: bool = False
514 ) -> bool:
515 """
516 Is the policy valid in the context of the ID types available in our
517 database?
519 Args:
520 valid_idnums: optional list of valid ID number types
521 verbose: report reasons to debug log
522 """
523 if valid_idnums is not None:
524 self.set_valid_idnums(valid_idnums)
525 if self._valid_for_idnums is None:
526 # Cache information
527 self.require_valid_idnum_info()
528 self._valid_for_idnums = self.is_valid_for_idnums(
529 self.valid_idnums, verbose=verbose
530 )
531 return self._valid_for_idnums
533 def is_valid_for_idnums(
534 self, valid_idnums: List[int], verbose: bool = False
535 ) -> bool:
536 """
537 Is the policy valid, given a list of valid ID number types?
539 Checks the following:
541 - valid syntax
542 - refers only to ID number types defined on the server
543 - is compatible with the tablet ID policy
545 Args:
546 valid_idnums: ID number types that are valid on the server
547 verbose: report reasons to debug log
548 """
549 # First, syntax:
550 if not self.is_syntactically_valid():
551 if verbose:
552 log.debug("is_valid_for_idnums(): Not syntactically valid")
553 return False
554 # Second, all ID numbers referred to by the policy exist:
555 for token in self.tokens:
556 if token > 0 and token not in valid_idnums:
557 if verbose:
558 log.debug(
559 "is_valid_for_idnums(): Refers to ID number type "
560 "{}, which does not exist",
561 token,
562 )
563 return False
564 if not self._compatible_with_tablet_id_policy(verbose=verbose):
565 if verbose:
566 log.debug(
567 "is_valid_for_idnums(): Less restrictive than the "
568 "tablet minimum ID policy; invalid"
569 )
570 return False
571 return True
573 # -------------------------------------------------------------------------
574 # Information about the ID number types the policy refers to
575 # -------------------------------------------------------------------------
577 def relevant_idnums(self, valid_idnums: List[int]) -> List[int]:
578 """
579 Which ID numbers are relevant to this policy?
581 Args:
582 valid_idnums: ID number types that are valid on the server
584 Returns:
585 the subset of ``valid_idnums`` that is mentioned somehow in the
586 policy
587 """
588 if not self.tokens:
589 return []
590 if TK_ANY_IDNUM in self.tokens or TK_OTHER_IDNUM in self.tokens:
591 # all are relevant
592 return valid_idnums
593 relevant_idnums = [] # type: List[int]
594 for which_idnum in valid_idnums:
595 assert which_idnum > 0, "Silly ID number types"
596 if which_idnum in self.tokens:
597 relevant_idnums.append(which_idnum)
598 return relevant_idnums
600 def specifically_mentioned_idnums(self) -> List[int]:
601 """
602 Returns the ID number tokens for all ID numbers mentioned in the
603 policy, as a list.
604 """
605 return [x for x in self.tokens if x > 0]
607 def contains_specific_idnum(self, which_idnum: int) -> bool:
608 """
609 Does the policy refer specifically to the given ID number type?
611 Args:
612 which_idnum: ID number type to test
613 """
614 assert which_idnum > 0
615 return which_idnum in self.tokens
617 # -------------------------------------------------------------------------
618 # More complex attributes
619 # -------------------------------------------------------------------------
621 def find_critical_single_numerical_id(
622 self, valid_idnums: List[int] = None, verbose: bool = False
623 ) -> Optional[int]:
624 """
625 If the policy involves a single mandatory ID number, return that ID
626 number; otherwise return None.
628 Args:
629 valid_idnums: ID number types that are valid on the server
630 verbose: report reasons to debug log
632 Returns:
633 int: the single critical ID number type, or ``None``
634 """
635 if not self.is_valid(valid_idnums):
636 if verbose:
637 log.debug("find_critical_single_numerical_id(): invalid")
638 return None
639 relevant_idnums = self.specifically_mentioned_idnums()
640 possible_critical_idnums = [] # type: List[int]
641 for which_idnum in relevant_idnums:
642 pip_with = PatientInfoPresence.make_uncaring_except(
643 which_idnum, Q_TRUE
644 )
645 satisfies_with_1 = self._value_for_pip(pip_with)
646 pip_with.present[TK_OTHER_IDNUM] = Q_FALSE
647 satisfies_with_2 = self._value_for_pip(pip_with)
648 pip_without = PatientInfoPresence.make_uncaring_except(
649 which_idnum, Q_FALSE
650 )
651 satisfies_without_1 = self._value_for_pip(pip_without)
652 pip_with.present[TK_OTHER_IDNUM] = Q_TRUE
653 satisfies_without_2 = self._value_for_pip(pip_without)
654 if verbose:
655 log.debug(
656 "... {}: satisfies_with={}, satisfies_without_1={}, "
657 "satisfies_without_2={}",
658 which_idnum,
659 satisfies_with_1,
660 satisfies_without_1,
661 satisfies_without_2,
662 )
663 if (
664 satisfies_with_1 is Q_TRUE
665 and satisfies_with_2 is Q_TRUE
666 and satisfies_without_1 is Q_FALSE
667 and satisfies_without_2 is Q_FALSE
668 ):
669 possible_critical_idnums.append(which_idnum)
670 if verbose:
671 log.debug(
672 "find_critical_single_numerical_id(): "
673 "possible_critical_idnums = {}",
674 possible_critical_idnums,
675 )
676 if len(possible_critical_idnums) == 1:
677 return possible_critical_idnums[0]
678 return None
680 def is_idnum_mandatory_in_policy(
681 self, which_idnum: int, valid_idnums: List[int], verbose: bool = False
682 ) -> bool:
683 """
684 Is the ID number mandatory in the specified policy?
686 Args:
687 which_idnum: ID number type to test
688 valid_idnums: ID number types that are valid on the server
689 verbose: report reasons to debug log
690 """
691 if which_idnum is None or which_idnum < 1:
692 if verbose:
693 log.debug("is_idnum_mandatory_in_policy(): bad ID type")
694 return False
695 if not self.contains_specific_idnum(which_idnum):
696 if verbose:
697 log.debug(
698 "is_idnum_mandatory_in_policy(): policy does not "
699 "contain ID {}, so not mandatory",
700 which_idnum,
701 )
702 return False
703 self.set_valid_idnums(valid_idnums)
704 if not self.is_valid():
705 if verbose:
706 log.debug("is_idnum_mandatory_in_policy(): policy invalid")
707 return False
709 pip_with = PatientInfoPresence.make_uncaring_except(
710 which_idnum, Q_TRUE
711 )
712 satisfies_with = self._value_for_pip(pip_with)
713 if satisfies_with != Q_TRUE:
714 if verbose:
715 log.debug(
716 "is_idnum_mandatory_in_policy(): policy not "
717 "satisfied by presence of ID {}, so not mandatory",
718 which_idnum,
719 )
720 return False
721 pip_without = PatientInfoPresence.make_uncaring_except(
722 which_idnum, Q_FALSE
723 )
724 satisfies_without = self._value_for_pip(pip_without)
725 if satisfies_without != Q_FALSE:
726 if verbose:
727 log.debug(
728 "is_idnum_mandatory_in_policy(): policy satisfied "
729 "without presence of ID {}, so not mandatory",
730 which_idnum,
731 )
732 return False
733 # Thus, if we get here, the policy is unhappy with the absence of our
734 # ID number type, but happy with it; therefore it is mandatory.
735 if verbose:
736 log.debug(
737 "is_idnum_mandatory_in_policy(): ID {} is mandatory",
738 which_idnum,
739 )
740 return True
742 def _requires_prohibits(
743 self, token: int, verbose: bool = False
744 ) -> Tuple[bool, bool]:
745 """
746 Does this policy require, and/or prohibit, a particular token?
748 Args:
749 token: token to check
750 verbose: report reasons to debug log
752 Returns:
753 tuple: requires, prohibits
754 """
755 pip_with = PatientInfoPresence.make_uncaring_except(token, Q_TRUE)
756 satisfies_with = self._value_for_pip(pip_with)
757 pip_without = PatientInfoPresence.make_uncaring_except(token, Q_FALSE)
758 satisfies_without = self._value_for_pip(pip_without)
759 requires = satisfies_with is Q_TRUE and satisfies_without is Q_FALSE
760 prohibits = satisfies_with is Q_FALSE and satisfies_without is Q_TRUE
761 if verbose:
762 log.debug(
763 "_requires_prohibits({t}): "
764 "satisfies_with={sw}, "
765 "satisfies_without={swo}, "
766 "requires={r}, "
767 "prohibits={p}",
768 t=token_to_str(token),
769 sw=satisfies_with,
770 swo=satisfies_without,
771 r=requires,
772 p=prohibits,
773 )
774 return requires, prohibits
776 def _requires_sex(self, verbose: bool = False) -> bool:
777 """
778 Does this policy require sex to be present?
780 Args:
781 verbose: report reasons to debug log
782 """
783 requires, _ = self._requires_prohibits(TK_SEX, verbose=verbose)
784 return requires
786 def _requires_an_idnum(self, verbose: bool = False) -> bool:
787 """
788 Does this policy require an ID number to be present?
790 Args:
791 verbose: report reasons to debug log
792 """
793 if verbose:
794 log.debug("_requires_an_idnum():")
795 for token in self.specifically_mentioned_idnums() + [
796 TK_ANY_IDNUM,
797 TK_OTHER_IDNUM,
798 ]:
799 requires, _ = self._requires_prohibits(token, verbose=verbose)
800 if requires:
801 if verbose:
802 log.debug(
803 "... requires ID number '{}'", token_to_str(token)
804 )
805 return True
806 return False
808 # def _less_restrictive_than(self, other: "TokenizedPolicy",
809 # valid_idnums: List[int],
810 # verbose: bool = False) -> bool:
811 # """
812 # Is this ("self") policy less restrictive than the "other" policy?
813 #
814 # "More restrictive" means "requires more information".
815 # "Less restrictive" means "requires or enforces less information".
816 #
817 # Therefore, we must return True if we can find a situation where
818 # "self" is satisfied but "other" is not.
819 #
820 # Args:
821 # other: the other policy
822 # valid_idnums: ID number types that are valid on the server
823 # verbose: report reasons to debug log
824 #
825 # This is very difficult. Abandoned this generic attempt in favour of a
826 # specific hard-coded check for the tablet policy.
827 # """
828 # if verbose:
829 # log.debug("_less_restrictive_than(): self={}, other={}",
830 # self, other)
831 # possible_tokens = valid_idnums + NON_IDNUM_INFO_TOKENS
832 # for token in possible_tokens:
833 # # Self
834 # self_requires, self_prohibits = self._requires_prohibits(
835 # token, valid_idnums)
836 # # Other
837 # pip_with = PatientInfoPresence.make_uncaring_except(
838 # token, Q_TRUE, valid_idnums)
839 # other_satisfies_with = other._value_for_pip(pip_with)
840 # pip_without = PatientInfoPresence.make_uncaring_except(
841 # token, Q_FALSE, valid_idnums)
842 # other_satisfies_without_1 = other._value_for_pip(pip_without)
843 # pip_without.special_anyidnum_present = Q_TRUE
844 # other_satisfies_without_2 = other._value_for_pip(pip_without)
845 # other_requires = (
846 # other_satisfies_with is Q_TRUE and
847 # other_satisfies_without_1 is Q_FALSE and
848 # other_satisfies_without_2 is Q_FALSE
849 # )
850 # other_prohibits = (
851 # other_satisfies_with is Q_FALSE and
852 # other_satisfies_without_1 is Q_TRUE and
853 # other_satisfies_without_2 is Q_TRUE
854 # )
855 # if verbose:
856 # log.debug(
857 # "... for {t}: "
858 # "self_requires={sr}, "
859 # "self_prohibits={sp}, "
860 # "other_satisfies_with={osw}, "
861 # "other_satisfies_without_1={oswo1}, "
862 # "other_satisfies_without_2={oswo2}, "
863 # "other_requires={or_}",
864 # "other_prohibits={op}",
865 # t=token_to_str(token),
866 # sr=self_requires,
867 # sp=self_prohibits,
868 # osw=other_satisfies_with,
869 # oswo1=other_satisfies_without_1,
870 # oswo2=other_satisfies_without_2,
871 # or_=other_requires,
872 # op=other_prohibits,
873 # )
874 #
875 # if other_requires and not self_requires:
876 # # The "self" policy is LESS RESTRICTIVE (requires less info).
877 # if verbose:
878 # log.debug(
879 # "... self does not require ID type {}, but other does " # noqa
880 # "require it; therefore self is less restrictive",
881 # token)
882 # return True
883 # # if self_prohibits and not other_prohibits:
884 # # # The "self" policy is LESS RESTRICTIVE (enforces less info). # noqa
885 # # if verbose:
886 # # log.debug(
887 # # "... self prohibits ID type {}, but other does not " # noqa
888 # # "prohibit it; therefore self is less restrictive",
889 # # token)
890 # # return True
891 # if verbose:
892 # log.debug(
893 # "... by elimination, self [{}] not less "
894 # "restrictive than other [{}]",
895 # self, other
896 # )
897 # return False
899 def _compatible_with_tablet_id_policy(self, verbose: bool = False) -> bool:
900 """
901 Is this policy compatible with :data:`TABLET_ID_POLICY`?
903 The "self" policy may be MORE restrictive than the tablet minimum ID
904 policy, but may not be LESS restrictive.
906 Args:
907 verbose: report reasons to debug log
909 Internal function -- doesn't used cached information.
910 """
911 # Method 1: abandoned.
912 # We previously used a version of _less_restrictive_than() that
913 # did a brute-force attempt, but that became prohibitive as ID numbers
914 # got added.
915 # A generic method is very hard (see above) -- not properly succeeded
916 # yet.
917 #
918 # return not self._less_restrictive_than(
919 # TABLET_ID_POLICY, valid_idnums, verbose=verbose)
921 # Method 2: manual.
922 if verbose:
923 log.debug("_compatible_with_tablet_id_policy():")
924 requires_sex = self._requires_sex(verbose=verbose)
925 if requires_sex:
926 if verbose:
927 log.debug("... requires sex")
928 else:
929 if verbose:
930 log.debug("... doesn't require sex; returning False")
931 return False
932 requires_an_idnum = self._requires_an_idnum(verbose=verbose)
933 if requires_an_idnum:
934 if verbose:
935 log.debug("... requires an ID number; returning True")
936 return True
937 if verbose:
938 log.debug("... does not require an ID number; trying alternatives")
939 other_mandatory = [TK_FORENAME, TK_SURNAME, TK_DOB, TK_EMAIL]
940 for token in other_mandatory:
941 requires, _ = self._requires_prohibits(token, verbose=verbose)
942 if not requires:
943 if verbose:
944 log.debug(
945 "... does not require '{}'; returning False",
946 token_to_str(token),
947 )
948 return False
949 log.debug(
950 "... requires all of {!r}; returning True",
951 [token_to_str(t) for t in other_mandatory],
952 )
953 return True
955 def compatible_with_tablet_id_policy(
956 self, valid_idnums: List[int], verbose: bool = False
957 ) -> bool:
958 """
959 Is this policy compatible with :data:`TABLET_ID_POLICY`?
961 The "self" policy may be MORE restrictive than the tablet minimum ID
962 policy, but may not be LESS restrictive.
964 Args:
965 valid_idnums: ID number types that are valid on the server
966 verbose: report reasons to debug log
967 """
968 self.set_valid_idnums(valid_idnums)
969 if not self.is_valid(verbose=verbose):
970 return False
971 return self._compatible_with_tablet_id_policy(verbose=verbose)
973 # -------------------------------------------------------------------------
974 # Check if a patient satisfies the policy
975 # -------------------------------------------------------------------------
977 def _value_for_ptinfo(self, ptinfo: BarePatientInfo) -> QuadState:
978 """
979 What does the policy evaluate to for a given patient info object?
981 Args:
982 ptinfo:
983 a `camcops_server.cc_modules.cc_simpleobjects.BarePatientInfo`
985 Returns:
986 a :class:`QuadState` quad-state value
987 """
988 pip = PatientInfoPresence.make_from_ptinfo(
989 ptinfo, self.specifically_mentioned_idnums()
990 )
991 return self._value_for_pip(pip)
993 def _value_for_pip(self, pip: PatientInfoPresence) -> QuadState:
994 """
995 What does the policy evaluate to for a given patient info presence
996 object?
998 Args:
999 pip:
1000 a `camcops_server.cc_modules.cc_simpleobjects.PatientInfoPresence`
1002 Returns:
1003 a :class:`QuadState` quad-state value
1004 """ # noqa
1006 def content_token_processor(token: int) -> QuadState:
1007 return self._element_value_test_pip(pip, token)
1009 return self._chunk_value(
1010 self.tokens, content_token_processor=content_token_processor
1011 )
1012 # ... which is recursive
1014 def satisfies_id_policy(self, ptinfo: BarePatientInfo) -> bool:
1015 """
1016 Does the patient information in ptinfo satisfy the specified ID policy?
1018 Args:
1019 ptinfo:
1020 a `camcops_server.cc_modules.cc_simpleobjects.BarePatientInfo`
1021 """
1022 return self._value_for_ptinfo(ptinfo) is Q_TRUE
1024 # -------------------------------------------------------------------------
1025 # Functions for the policy to parse itself and compare itself to a patient
1026 # -------------------------------------------------------------------------
1028 def _chunk_value(
1029 self,
1030 tokens: TOKENIZED_POLICY_TYPE,
1031 content_token_processor: CONTENT_TOKEN_PROCESSOR_TYPE,
1032 ) -> QuadState:
1033 """
1034 Applies the policy to the patient info in ``ptinfo``.
1035 Can be used recursively.
1037 Args:
1038 tokens:
1039 a tokenized policy
1040 content_token_processor:
1041 a function to be called for each "content" token, which returns
1042 its Boolean value, or ``None`` in case of failure
1044 Returns:
1045 a :class:`QuadState` quad-state value
1046 """
1047 want_content = True
1048 processing_and = False
1049 processing_or = False
1050 index = 0
1051 value = None # type: Optional[QuadState]
1052 while index < len(tokens):
1053 if want_content:
1054 nextchunk, index = self._content_chunk_value(
1055 tokens, index, content_token_processor
1056 )
1057 if nextchunk is Q_ERROR:
1058 return Q_ERROR # fail
1059 if value is None:
1060 value = nextchunk
1061 elif processing_and:
1062 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1063 # implement logical AND
1064 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1065 value = quad_and(value, nextchunk)
1066 elif processing_or:
1067 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1068 # implement logical OR
1069 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1070 value = quad_or(value, nextchunk)
1071 else:
1072 # Error; shouldn't get here
1073 return Q_ERROR
1074 processing_and = False
1075 processing_or = False
1076 else:
1077 # Want operator
1078 operator, index = self._op(tokens, index)
1079 if operator is None:
1080 return Q_ERROR # fail
1081 if operator == TK_AND:
1082 processing_and = True
1083 elif operator == TK_OR:
1084 processing_or = True
1085 else:
1086 # Error; shouldn't get here
1087 return Q_ERROR
1088 want_content = not want_content
1089 if want_content:
1090 log.debug("_chunk_value(): ended wanting content; bad policy")
1091 return Q_ERROR
1092 return value
1094 def _content_chunk_value(
1095 self,
1096 tokens: TOKENIZED_POLICY_TYPE,
1097 start: int,
1098 content_token_processor: CONTENT_TOKEN_PROCESSOR_TYPE,
1099 ) -> Tuple[QuadState, int]:
1100 """
1101 Applies part of a policy to ``ptinfo``. The part of policy pointed to
1102 by ``start`` represents something -- "content" -- that should return a
1103 value (not an operator, for example). Called by :func:`id_policy_chunk`
1104 (q.v.).
1106 Args:
1107 tokens:
1108 a tokenized policy (list of integers)
1109 start:
1110 zero-based index of the first token to check
1111 content_token_processor:
1112 a function to be called for each "content" token, which returns
1113 its Boolean value, or ``None`` in case of failure
1115 Returns:
1116 tuple: chunk_value, next_index. ``chunk_value`` is ``True`` if the
1117 specified chunk is satisfied by the ``ptinfo``, ``False`` if it
1118 isn't, and ``None`` if there was an error. ``next_index`` is the
1119 index of the next token after this chunk.
1121 """
1122 if start >= len(tokens):
1123 log.debug(
1124 "_content_chunk_value(): " "beyond end of policy; bad policy"
1125 )
1126 return Q_ERROR, start
1127 token = tokens[start]
1128 if token in (TK_RPAREN, TK_AND, TK_OR):
1129 log.debug(
1130 "_content_chunk_value(): "
1131 "chunk starts with ), AND, or OR; bad policy"
1132 )
1133 return Q_ERROR, start
1134 elif token == TK_LPAREN:
1135 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1136 # implement parentheses
1137 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1138 subchunkstart = start + 1 # exclude the opening bracket
1139 # Find closing parenthesis
1140 depth = 1
1141 searchidx = subchunkstart
1142 while depth > 0:
1143 if searchidx >= len(tokens):
1144 log.debug(
1145 "_content_chunk_value(): "
1146 "Unmatched left parenthesis; bad policy"
1147 )
1148 return Q_ERROR, start
1149 elif tokens[searchidx] == TK_LPAREN:
1150 depth += 1
1151 elif tokens[searchidx] == TK_RPAREN:
1152 depth -= 1
1153 searchidx += 1
1154 subchunkend = searchidx - 1
1155 # ... to exclude the closing bracket from the analysed subchunk
1156 chunk_value = self._chunk_value(
1157 tokens[subchunkstart:subchunkend], content_token_processor
1158 )
1159 return (
1160 chunk_value,
1161 subchunkend + 1,
1162 ) # to move past the closing bracket # noqa
1163 elif token == TK_NOT:
1164 next_value, next_index = self._content_chunk_value(
1165 tokens, start + 1, content_token_processor
1166 )
1167 if next_value is Q_ERROR:
1168 return next_value, start
1169 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1170 # implement logical NOT
1171 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1172 return quad_not(next_value), next_index
1173 else:
1174 # meaningful token
1175 return content_token_processor(token), start + 1
1177 @classmethod
1178 def _op(
1179 cls, policy: TOKENIZED_POLICY_TYPE, start: int
1180 ) -> Tuple[Optional[TOKEN_TYPE], int]:
1181 """
1182 Returns an operator from the policy, beginning at index ``start``, or
1183 ``None`` if there wasn't an operator there.
1185 policy:
1186 a tokenized policy (list of integers)
1187 start:
1188 zero-based index of the first token to check
1190 Returns:
1191 tuple: ``operator, next_index``. ``operator`` is the operator's
1192 integer token or ``None``. ``next_index`` gives the next index of
1193 the policy to check at.
1194 """
1195 if start >= len(policy):
1196 log.debug("_op(): beyond end of policy")
1197 return None, start
1198 token = policy[start]
1199 if token in (TK_AND, TK_OR):
1200 return token, start + 1
1201 else:
1202 log.debug("_op(): not an operator; bad policy")
1203 # Not an operator
1204 return None, start
1206 # Things to do with content tokens 1: are they present in patient info?
1208 @staticmethod
1209 def _element_value_test_pip(
1210 pip: PatientInfoPresence, token: TOKEN_TYPE
1211 ) -> QuadState:
1212 """
1213 Returns the "value" of a content token as judged against the patient
1214 information. For example, if the patient information contains a date of
1215 birth, a ``TK_DOB`` token will evaluate to ``True``.
1217 Args:
1218 pip:
1219 a `camcops_server.cc_modules.cc_simpleobjects.PatientInfoPresence`
1220 token:
1221 an integer token from the policy
1223 Returns:
1224 a :class:`QuadState` quad-state value
1225 """ # noqa
1226 assert is_info_token(token)
1227 if token == TK_ANY_IDNUM:
1228 return pip.any_idnum_present()
1229 else:
1230 return pip.is_present(token)
1233# =============================================================================
1234# Tablet ID policy
1235# =============================================================================
1237TABLET_ID_POLICY_STR = "sex AND ((forename AND surname AND dob) OR anyidnum)"
1238TABLET_ID_POLICY = TokenizedPolicy(TABLET_ID_POLICY_STR)