aboutsummaryrefslogtreecommitdiff
path: root/generalresearch/models/lucid/survey.py
diff options
context:
space:
mode:
Diffstat (limited to 'generalresearch/models/lucid/survey.py')
-rw-r--r--generalresearch/models/lucid/survey.py105
1 files changed, 105 insertions, 0 deletions
diff --git a/generalresearch/models/lucid/survey.py b/generalresearch/models/lucid/survey.py
new file mode 100644
index 0000000..6d81254
--- /dev/null
+++ b/generalresearch/models/lucid/survey.py
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+from typing import Optional, Dict, Set, Tuple, List
+
+from pydantic import NonNegativeInt, Field, ConfigDict, BaseModel
+
+from generalresearch.models import Source
+from generalresearch.models.custom_types import (
+ AwareDatetimeISO,
+ UUIDStr,
+ CoercedStr,
+ BigAutoInteger,
+)
+from generalresearch.models.thl.locales import CountryISO, LanguageISO
+from generalresearch.models.thl.survey.condition import (
+ MarketplaceCondition,
+ ConditionValueType,
+)
+
+
+class LucidCondition(MarketplaceCondition):
+ model_config = ConfigDict(populate_by_name=True, frozen=False, extra="ignore")
+
+ id: BigAutoInteger = Field()
+ source: Source = Field(default=Source.LUCID)
+ question_id: Optional[CoercedStr] = Field(
+ min_length=1,
+ max_length=16,
+ pattern=r"^[0-9]+$",
+ )
+ country_iso: CountryISO = Field()
+ language_iso: LanguageISO = Field()
+
+ @property
+ def criterion_hash(self) -> None:
+ # We use the integer ID throughout. Make sure we don't accidentally use this
+ raise ValueError()
+
+ def __hash__(self):
+ # this is so it can be put into a set / dictionary key
+ return hash(self.id)
+
+ @classmethod
+ def from_mysql(cls, x):
+ x["value_type"] = ConditionValueType.LIST
+ x["negate"] = False
+ x["values"] = x.pop("pre_codes").split("|")
+ x["question_id"] = str(x["question_id"])
+ return cls.model_validate(x)
+
+
+class LucidQualification(BaseModel):
+ criterion: int = Field()
+ modified: AwareDatetimeISO = Field(description="modified or created")
+
+
+class LucidQuota(BaseModel):
+ id: BigAutoInteger = Field()
+ uuid: UUIDStr = Field()
+ upper_limit: NonNegativeInt = Field(examples=[20])
+ criteria: List[int] = Field(min_length=1, max_length=25)
+ modified: AwareDatetimeISO = Field(description="modified or created")
+ # We'll look this up with a special mysql query. If None, it means
+ # that we don't know.
+ finish_count: Optional[int] = Field(default=None)
+
+ def __hash__(self):
+ return hash(self.id)
+
+ @property
+ def is_open(self) -> bool:
+ return self.upper_limit > self.finish_count
+
+ def passes(self, criteria_evaluation: Dict[int, Optional[bool]]) -> bool:
+ # Passes means we 1) meet all conditions (aka "match") AND 2) the quota is open.
+ return self.is_open and self.matches(criteria_evaluation)
+
+ def matches(self, criteria_evaluation: Dict[int, Optional[bool]]) -> bool:
+ # Matches means we meet all conditions.
+ # We can "match" a quota that is closed. In that case, we would not be eligible for the survey.
+ return all(criteria_evaluation.get(c) for c in self.criteria)
+
+ # def matches_optional(
+ # self, criteria_evaluation: Dict[int, Optional[bool]]
+ # ) -> Optional[bool]:
+ # # We need to know if any conditions are unknown to avoid matching a full quota. If any fail,
+ # # then we know we fail regardless of any being unknown.
+ # evals = [criteria_evaluation.get(c) for c in self.criteria]
+ # if False in evals:
+ # return False
+ # if None in evals:
+ # return None
+ # return True
+
+ def matches_soft(
+ self, criteria_evaluation: Dict[int, Optional[bool]]
+ ) -> Tuple[Optional[bool], Set[int]]:
+ # Passes back "matches" (T/F/none) and a list of unknown criterion hashes
+ hash_evals = {cell: criteria_evaluation.get(cell) for cell in self.criteria}
+ evals = set(hash_evals.values())
+ if False in evals:
+ return False, set()
+ if None in evals:
+ return None, {cell for cell, ev in hash_evals.items() if ev is None}
+ return True, set()