Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DataStores class to questionnaire store #1228

Merged
merged 30 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
22ea452
Add initial changes
petechd Oct 16, 2023
cfc0537
Fix remaining tests
petechd Oct 16, 2023
afdadfc
Remove logic from can_access_location
petechd Oct 17, 2023
698a6d8
Add data stores to summary question
petechd Oct 17, 2023
c279dd2
Use data stores placeholder renderer
petechd Oct 17, 2023
e34553b
Refactor converter and its tests
petechd Oct 18, 2023
f6b1b51
Use default factory on all in DataStores class
petechd Oct 18, 2023
58f26a2
Move data stores to separate module
petechd Oct 23, 2023
a2d9bfd
Use datastore in remaining places, remove helper methods
petechd Oct 23, 2023
7fb217a
Fix progress store import
petechd Oct 24, 2023
6208c21
Merge branch 'main' into add-data-stores-class-to-questionnaire-store
petechd Oct 24, 2023
f0e09cb
Revert can_access_location condition
petechd Oct 24, 2023
977db5f
Remove commented out code, redundant vars
petechd Oct 25, 2023
b6b5b0b
Merge branch 'main' into add-data-stores-class-to-questionnaire-store
petechd Oct 25, 2023
c325439
Fix merge conflicts issues after introduction of return location
petechd Oct 25, 2023
fff4632
Fix missing test code logic, sort some imports and some more tests
petechd Oct 25, 2023
626772c
Add misc review changes
petechd Oct 25, 2023
82d61d0
Format files
petechd Oct 25, 2023
60f8019
Initialise data stores only once in test_router
petechd Oct 25, 2023
e2476aa
Merge branch 'main' into add-data-stores-class-to-questionnaire-store
petechd Oct 26, 2023
e1f73fe
Remove type ignore
petechd Oct 26, 2023
e487099
Fix param calls in test methods
petechd Oct 27, 2023
830d0fa
Merge branch 'main' into add-data-stores-class-to-questionnaire-store
petechd Oct 30, 2023
9b3cca9
Fix comment
petechd Oct 30, 2023
df90526
Remove comment
petechd Nov 6, 2023
2be9dd5
Rename data_stores to stores in QS
petechd Nov 6, 2023
b3add18
Rename stores
petechd Nov 6, 2023
990214f
Add review fixes
petechd Nov 9, 2023
1890362
Merge branch 'main' into add-data-stores-class-to-questionnaire-store
petechd Nov 9, 2023
74d8ce7
Merge branch 'main' into add-data-stores-class-to-questionnaire-store
petechd Nov 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions app/data_models/data_stores.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from dataclasses import dataclass, field
from typing import MutableMapping

from app.data_models.answer_store import AnswerStore
from app.data_models.list_store import ListStore
from app.data_models.metadata_proxy import MetadataProxy
from app.data_models.progress_store import ProgressStore
from app.data_models.supplementary_data_store import SupplementaryDataStore


@dataclass
class DataStores:
MebinAbraham marked this conversation as resolved.
Show resolved Hide resolved
MebinAbraham marked this conversation as resolved.
Show resolved Hide resolved
# self.metadata is a read-only view over self._metadata
metadata: MetadataProxy | None = None
MebinAbraham marked this conversation as resolved.
Show resolved Hide resolved
response_metadata: MutableMapping = field(default_factory=dict)
list_store: ListStore = field(default_factory=ListStore)
answer_store: AnswerStore = field(default_factory=AnswerStore)
progress_store: ProgressStore = field(default_factory=ProgressStore)
supplementary_data_store: SupplementaryDataStore = field(
default_factory=SupplementaryDataStore
)
64 changes: 34 additions & 30 deletions app/data_models/questionnaire_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import TYPE_CHECKING, MutableMapping, Optional

from app.data_models.answer_store import AnswerStore
from app.data_models.data_stores import DataStores
from app.data_models.list_store import ListStore
from app.data_models.metadata_proxy import MetadataProxy
from app.data_models.progress_store import ProgressStore
Expand All @@ -29,13 +30,7 @@ def __init__(
version = self.get_latest_version_number()
self.version = version
self._metadata: MutableMapping = {}
# self.metadata is a read-only view over self._metadata
self.metadata: MetadataProxy | None = None
self.response_metadata: MutableMapping = {}
self.list_store = ListStore()
self.answer_store = AnswerStore()
self.progress_store = ProgressStore()
self.supplementary_data_store = SupplementaryDataStore()
self.data_stores = DataStores()
self.submitted_at: Optional[datetime]
self.collection_exercise_sid: Optional[str]

Expand All @@ -60,7 +55,7 @@ def set_metadata(self, to_set: MutableMapping) -> QuestionnaireStore:
Metadata should normally be read only.
"""
self._metadata = to_set
self.metadata = MetadataProxy.from_dict(self._metadata)
self.data_stores.metadata = MetadataProxy.from_dict(self._metadata)

return self

Expand All @@ -72,7 +67,7 @@ def set_supplementary_data(self, to_set: MutableMapping) -> None:
this updates ListStore to add/update any lists for supplementary data and stores the
identifier -> list_item_id mappings in the supplementary data store to use in the payload at the end
"""
if self.supplementary_data_store.list_mappings:
if self.data_stores.supplementary_data_store.list_mappings:
MebinAbraham marked this conversation as resolved.
Show resolved Hide resolved
self._remove_old_supplementary_lists_and_answers(new_data=to_set)

list_mappings = {
Expand All @@ -82,7 +77,7 @@ def set_supplementary_data(self, to_set: MutableMapping) -> None:
for list_name, list_data in to_set.get("items", {}).items()
}

self.supplementary_data_store = SupplementaryDataStore(
self.data_stores.supplementary_data_store = SupplementaryDataStore(
supplementary_data=to_set, list_mappings=list_mappings
)

Expand All @@ -99,11 +94,13 @@ def _create_supplementary_list(
# if any pre-existing supplementary data already has a mapping for this list item
# then its already in the list store and doesn't require adding
if not (
list_item_id := self.supplementary_data_store.list_lookup.get(
list_item_id := self.data_stores.supplementary_data_store.list_lookup.get(
list_name, {}
).get(identifier)
).get(
identifier
)
):
list_item_id = self.list_store.add_list_item(list_name)
list_item_id = self.data_stores.list_store.add_list_item(list_name)
list_mappings.append(
SupplementaryDataListMapping(
identifier=identifier, list_item_id=list_item_id
Expand All @@ -120,48 +117,55 @@ def _remove_old_supplementary_lists_and_answers(
:param new_data - the new supplementary data for comparison
"""
deleted_list_item_ids: set[str] = set()
for list_name, mappings in self.supplementary_data_store.list_lookup.items():
for (
list_name,
mappings,
) in self.data_stores.supplementary_data_store.list_lookup.items():
if list_name in new_data.get("items", {}):
new_identifiers = [
item["identifier"] for item in new_data["items"][list_name]
]
for identifier, list_item_id in mappings.items():
if identifier not in new_identifiers:
self.list_store.delete_list_item(list_name, list_item_id)
self.data_stores.list_store.delete_list_item(
list_name, list_item_id
)
deleted_list_item_ids.add(list_item_id)
else:
self.list_store.delete_list(list_name)
self.data_stores.list_store.delete_list(list_name)
deleted_list_item_ids.update(mappings.values())
self.answer_store.remove_all_answers_for_list_item_ids(*deleted_list_item_ids)
self.data_stores.answer_store.remove_all_answers_for_list_item_ids(
*deleted_list_item_ids
)

def _deserialize(self, data: str) -> None:
json_data = json_loads(data)
self.progress_store = ProgressStore(json_data.get("PROGRESS"))
self.data_stores.progress_store = ProgressStore(json_data.get("PROGRESS"))
self.set_metadata(json_data.get("METADATA", {}))
self.supplementary_data_store = SupplementaryDataStore.deserialize(
self.data_stores.supplementary_data_store = SupplementaryDataStore.deserialize(
json_data.get("SUPPLEMENTARY_DATA", {})
)
self.answer_store = AnswerStore(json_data.get("ANSWERS"))
self.list_store = ListStore.deserialize(json_data.get("LISTS"))
self.response_metadata = json_data.get("RESPONSE_METADATA", {})
self.data_stores.answer_store = AnswerStore(json_data.get("ANSWERS"))
self.data_stores.list_store = ListStore.deserialize(json_data.get("LISTS"))
self.data_stores.response_metadata = json_data.get("RESPONSE_METADATA", {})

def serialize(self) -> str:
data = {
"METADATA": self._metadata,
MebinAbraham marked this conversation as resolved.
Show resolved Hide resolved
"ANSWERS": list(self.answer_store),
"SUPPLEMENTARY_DATA": self.supplementary_data_store.serialize(),
"LISTS": self.list_store.serialize(),
"PROGRESS": self.progress_store.serialize(),
"RESPONSE_METADATA": self.response_metadata,
"ANSWERS": list(self.data_stores.answer_store),
"SUPPLEMENTARY_DATA": self.data_stores.supplementary_data_store.serialize(),
"LISTS": self.data_stores.list_store.serialize(),
katie-gardner marked this conversation as resolved.
Show resolved Hide resolved
"PROGRESS": self.data_stores.progress_store.serialize(),
"RESPONSE_METADATA": self.data_stores.response_metadata,
}
return json_dumps(data)

def delete(self) -> None:
self._storage.delete()
self._metadata.clear()
self.response_metadata = {}
self.answer_store.clear()
self.progress_store.clear()
self.data_stores.response_metadata = {}
self.data_stores.answer_store.clear()
katie-gardner marked this conversation as resolved.
Show resolved Hide resolved
self.data_stores.progress_store.clear()

def save(self) -> None:
data = self.serialize()
Expand Down
109 changes: 21 additions & 88 deletions app/forms/questionnaire_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,14 @@
from collections.abc import Callable
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Mapping, MutableMapping, Optional, Sequence, Union
from typing import Any, Mapping, Optional, Sequence, Union

from dateutil.relativedelta import relativedelta
from flask_wtf import FlaskForm
from werkzeug.datastructures import ImmutableMultiDict, MultiDict
from wtforms import validators

from app.data_models import (
AnswerStore,
ListStore,
ProgressStore,
SupplementaryDataStore,
)
from app.data_models.metadata_proxy import MetadataProxy
from app.data_models.data_stores import DataStores
from app.forms import error_messages
from app.forms.field_handlers import DateHandler, FieldHandler, get_field_handler
from app.forms.validators import DateRangeCheck, MutuallyExclusiveCheck, SumCheck
Expand Down Expand Up @@ -50,37 +44,23 @@ def __init__(
self,
schema: QuestionnaireSchema,
question_schema: QuestionSchemaType,
answer_store: AnswerStore,
list_store: ListStore,
metadata: Optional[MetadataProxy],
response_metadata: MutableMapping,
data_stores: DataStores,
location: Union[None, Location, RelationshipLocation],
progress_store: ProgressStore,
supplementary_data_store: SupplementaryDataStore,
**kwargs: Union[MultiDict, Mapping, None],
):
self.schema = schema
self.question = question_schema
self.answer_store = answer_store
self.list_store = list_store
self.metadata = metadata
self.response_metadata = response_metadata
self.location = location
self.question_errors: dict[str, str] = {}
self.options_with_detail_answer: dict = {}
self.question_title = self.question.get("title", "")
self.progress_store = progress_store
self.supplementary_data_store = supplementary_data_store
self.data_stores = data_stores

self.value_source_resolver = ValueSourceResolver(
answer_store=self.answer_store,
schema=self.schema,
metadata=self.metadata,
response_metadata=self.response_metadata,
list_store=self.list_store,
data_stores=data_stores,
location=self.location,
list_item_id=self.location.list_item_id if self.location else None,
progress_store=self.progress_store,
supplementary_data_store=self.supplementary_data_store,
)

super().__init__(**kwargs)
Expand Down Expand Up @@ -219,7 +199,7 @@ def _get_target_total_and_currency(
target_answer = self.schema.get_answers_by_answer_id(
calculation["answer_id"]
)[0]
target_total = self.answer_store.get_answer(
target_total = self.data_stores.answer_store.get_answer(
calculation["answer_id"]
).value # type: ignore # expect not None

Expand Down Expand Up @@ -331,27 +311,18 @@ def _get_period_range_for_single_date(
) -> timedelta:
list_item_id = self.location.list_item_id if self.location else None
value_source_resolver = ValueSourceResolver(
answer_store=self.answer_store,
list_store=self.list_store,
metadata=self.metadata,
response_metadata=self.response_metadata,
data_stores=self.data_stores,
schema=self.schema,
location=self.location,
list_item_id=list_item_id,
escape_answer_values=False,
progress_store=self.progress_store,
supplementary_data_store=self.supplementary_data_store,
)

rule_evaluator = RuleEvaluator(
data_stores=self.data_stores,
schema=self.schema,
answer_store=self.answer_store,
list_store=self.list_store,
metadata=self.metadata,
response_metadata=self.response_metadata,
location=self.location,
progress_store=self.progress_store,
supplementary_data_store=self.supplementary_data_store,
# Type ignore: location in rule_evaluator can be both Location or RelationshipLocation type but is only Location type here
location=self.location, # type: ignore
MebinAbraham marked this conversation as resolved.
Show resolved Hide resolved
)

handler = DateHandler(
Expand Down Expand Up @@ -417,7 +388,7 @@ def _get_formatted_calculation_values(
block_id = self.location.block_id if self.location else None
if block_id and block_id in self.schema.dynamic_answers_parent_block_ids:
list_name = self.schema.get_list_name_for_dynamic_answer(block_id)
list_item_ids = self.list_store[list_name]
list_item_ids = self.data_stores.list_store[list_name]
for answer_id in answers_sequence:
if self.schema.is_answer_dynamic(answer_id):
answers_list.extend(
Expand Down Expand Up @@ -492,32 +463,19 @@ def get_answer_fields(
question: QuestionSchemaType,
data: MultiDict[str, Any] | Mapping[str, Any] | None,
schema: QuestionnaireSchema,
answer_store: AnswerStore,
list_store: ListStore,
metadata: MetadataProxy | None,
response_metadata: MutableMapping,
data_stores: DataStores,
location: LocationType | None,
progress_store: ProgressStore,
supplementary_data_store: SupplementaryDataStore,
) -> dict[str, FieldHandler]:
list_item_id = location.list_item_id if location else None

block_ids_by_section: dict[SectionKey, tuple[str, ...]] = {}

if location and progress_store:
if location and data_stores.progress_store:
block_ids_by_section = (
get_routing_path_block_ids_by_section_for_calculated_summary_dependencies(
location=location,
progress_store=progress_store,
path_finder=PathFinder(
schema=schema,
answer_store=answer_store,
list_store=list_store,
progress_store=progress_store,
metadata=metadata,
response_metadata=response_metadata,
supplementary_data_store=supplementary_data_store,
),
progress_store=data_stores.progress_store,
path_finder=PathFinder(schema=schema, data_stores=data_stores),
data=question,
ignore_keys=["when"],
schema=schema,
Expand All @@ -529,29 +487,19 @@ def get_answer_fields(

def _get_value_source_resolver(list_item: str | None = None) -> ValueSourceResolver:
return ValueSourceResolver(
answer_store=answer_store,
list_store=list_store,
metadata=metadata,
data_stores=data_stores,
schema=schema,
location=location,
list_item_id=list_item,
escape_answer_values=False,
response_metadata=response_metadata,
routing_path_block_ids=block_ids,
assess_routing_path=False,
progress_store=progress_store,
supplementary_data_store=supplementary_data_store,
)

rule_evaluator = RuleEvaluator(
schema=schema,
answer_store=answer_store,
list_store=list_store,
metadata=metadata,
response_metadata=response_metadata,
data_stores=data_stores,
location=location,
progress_store=progress_store,
supplementary_data_store=supplementary_data_store,
)

answer_fields = {}
Expand Down Expand Up @@ -653,12 +601,7 @@ def generate_form(
*,
schema: QuestionnaireSchema,
question_schema: QuestionSchemaType,
answer_store: AnswerStore,
list_store: ListStore,
metadata: MetadataProxy | None,
response_metadata: MutableMapping,
progress_store: ProgressStore,
supplementary_data_store: SupplementaryDataStore,
data_stores: DataStores,
location: LocationType | None = None,
data: dict[str, Any] | None = None,
form_data: MultiDict | None = None,
Expand All @@ -674,13 +617,8 @@ class DynamicForm(QuestionnaireForm):
question_schema,
input_data,
schema,
answer_store,
list_store,
metadata,
response_metadata,
data_stores,
location,
progress_store=progress_store,
supplementary_data_store=supplementary_data_store,
)

for answer_id, field in answer_fields.items():
Expand All @@ -689,13 +627,8 @@ class DynamicForm(QuestionnaireForm):
return DynamicForm(
schema,
question_schema,
answer_store,
list_store,
metadata,
response_metadata,
data_stores,
location,
data=data,
formdata=form_data,
progress_store=progress_store,
supplementary_data_store=supplementary_data_store,
)
2 changes: 1 addition & 1 deletion app/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def get_metadata(user: User) -> MetadataProxy | None:
return None

questionnaire_store = get_questionnaire_store(user.user_id, user.user_ik)
return questionnaire_store.metadata
return questionnaire_store.data_stores.metadata


def get_view_submitted_response_expiration_time(submitted_at: datetime) -> datetime:
Expand Down
Loading