\n"
@@ -41,9 +41,9 @@ msgstr ""
msgid "%(from_date)s to %(to_date)s"
msgstr ""
-#: app/jinja_filters.py:701
-#: templates/partials/summary/collapsible-summary.html:26
-#: templates/partials/summary/summary.html:22
+#: app/jinja_filters.py:716
+#: templates/partials/summary/collapsible-summary.html:27
+#: templates/partials/summary/summary.html:25
msgid "No answer provided"
msgstr ""
@@ -456,7 +456,7 @@ msgstr ""
#: templates/individual_response/confirmation-post.html:21
#: templates/individual_response/confirmation-text-message.html:29
#: templates/individual_response/question.html:5 templates/interstitial.html:8
-#: templates/sectionsummary.html:10 templates/sectionsummary.html:50
+#: templates/sectionsummary.html:10 templates/sectionsummary.html:48
msgid "Continue"
msgstr ""
@@ -546,7 +546,7 @@ msgstr ""
#: app/views/handlers/confirm_email.py:129
#: app/views/handlers/confirmation_email.py:67
-#: app/views/handlers/feedback.py:84 app/views/handlers/question.py:170
+#: app/views/handlers/feedback.py:84 app/views/handlers/question.py:185
msgid "Error: {page_title}"
msgstr ""
@@ -735,14 +735,6 @@ msgstr ""
msgid "View Submitted Response"
msgstr ""
-#: templates/calculatedsummary.html:13
-msgid "Please review your answers and confirm these are correct"
-msgstr ""
-
-#: templates/calculatedsummary.html:24
-msgid "Yes, I confirm these are correct"
-msgstr ""
-
#: templates/census-thank-you.html:9 templates/confirmation-email.html:8
msgid "There is a problem with this page"
msgstr ""
@@ -871,7 +863,7 @@ msgstr ""
msgid "If you can’t answer someone else’s questions"
msgstr ""
-#: templates/interstitial.html:23 templates/partials/question.html:45
+#: templates/interstitial.html:23 templates/partials/question.html:49
msgid "If you can’t answer questions for this person"
msgstr ""
@@ -930,13 +922,13 @@ msgid "Save questions as PDF"
msgstr ""
#: templates/partials/introduction/preview.html:37
-#: templates/partials/summary/collapsible-summary.html:49
+#: templates/partials/summary/collapsible-summary.html:51
#: templates/preview.html:92
msgid "Show all"
msgstr ""
#: templates/partials/introduction/preview.html:38
-#: templates/partials/summary/collapsible-summary.html:50
+#: templates/partials/summary/collapsible-summary.html:52
#: templates/preview.html:93
msgid "Hide all"
msgstr ""
@@ -1323,6 +1315,14 @@ msgstr ""
msgid "Continue survey"
msgstr ""
+#: templates/layouts/_calculatedsummary.html:14
+msgid "Please review your answers and confirm these are correct"
+msgstr ""
+
+#: templates/layouts/_calculatedsummary.html:25
+msgid "Yes, I confirm these are correct"
+msgstr ""
+
#: templates/layouts/_questionnaire.html:39
msgid "Choose another section and return to this later"
msgstr ""
@@ -1382,7 +1382,7 @@ msgid ""
msgstr ""
#: templates/partials/preview-question.html:34
-#: templates/partials/question.html:81
+#: templates/partials/question.html:85
msgid "Or"
msgstr ""
@@ -1390,19 +1390,19 @@ msgstr ""
msgid "{max_characters} characters can be added."
msgstr ""
-#: templates/partials/question.html:72
+#: templates/partials/question.html:76
msgid "Selecting this will clear your answer"
msgstr ""
-#: templates/partials/question.html:73
+#: templates/partials/question.html:77
msgid "cleared"
msgstr ""
-#: templates/partials/question.html:76
+#: templates/partials/question.html:80
msgid "Selecting this will deselect any selected options"
msgstr ""
-#: templates/partials/question.html:77 templates/partials/question.html:85
+#: templates/partials/question.html:81 templates/partials/question.html:89
msgid "deselected"
msgstr ""
@@ -1568,14 +1568,14 @@ msgstr ""
msgid "Start survey"
msgstr ""
-#: templates/partials/summary/collapsible-summary.html:27
+#: templates/partials/summary/collapsible-summary.html:28
#: templates/partials/summary/list-summary.html:7
-#: templates/partials/summary/summary.html:25
+#: templates/partials/summary/summary.html:28
msgid "Change"
msgstr ""
-#: templates/partials/summary/collapsible-summary.html:28
-#: templates/partials/summary/summary.html:26
+#: templates/partials/summary/collapsible-summary.html:29
+#: templates/partials/summary/summary.html:29
msgid "Change your answer for:"
msgstr ""
@@ -1584,7 +1584,7 @@ msgid "Change details for {item_name}"
msgstr ""
#: templates/partials/summary/list-summary.html:9
-#: templates/partials/summary/summary.html:23
+#: templates/partials/summary/summary.html:26
msgid "Remove"
msgstr ""
@@ -1592,7 +1592,7 @@ msgstr ""
msgid "Remove {item_name}"
msgstr ""
-#: templates/partials/summary/summary.html:24
+#: templates/partials/summary/summary.html:27
msgid "Remove your answer for:"
msgstr ""
diff --git a/app/views/contexts/__init__.py b/app/views/contexts/__init__.py
index 6477a330bc..1acb951d53 100644
--- a/app/views/contexts/__init__.py
+++ b/app/views/contexts/__init__.py
@@ -1,5 +1,6 @@
from .calculated_summary_context import CalculatedSummaryContext
from .context import Context
+from .grand_calculated_summary_context import GrandCalculatedSummaryContext
from .hub_context import HubContext
from .list_context import ListContext
from .section_summary_context import SectionSummaryContext
@@ -7,6 +8,7 @@
__all__ = [
"CalculatedSummaryContext",
+ "GrandCalculatedSummaryContext",
"Context",
"SubmitQuestionnaireContext",
"HubContext",
diff --git a/app/views/contexts/calculated_summary_context.py b/app/views/contexts/calculated_summary_context.py
index bbe379ed59..b82822a919 100644
--- a/app/views/contexts/calculated_summary_context.py
+++ b/app/views/contexts/calculated_summary_context.py
@@ -1,16 +1,4 @@
-from copy import deepcopy
-from decimal import Decimal
-from typing import (
- Any,
- Callable,
- Iterable,
- Literal,
- Mapping,
- MutableMapping,
- Optional,
- Tuple,
- Union,
-)
+from typing import Callable, Iterable, Literal, Mapping, MutableMapping, Tuple
from werkzeug.datastructures import ImmutableDict
@@ -33,6 +21,7 @@
from app.questionnaire.value_source_resolver import ValueSourceResolver
from app.questionnaire.variants import choose_question_to_display, transform_variants
from app.views.contexts.context import Context
+from app.views.contexts.summary.calculated_summary_block import NumericType
from app.views.contexts.summary.group import Group
@@ -44,10 +33,12 @@ def __init__(
answer_store: AnswerStore,
list_store: ListStore,
progress_store: ProgressStore,
- metadata: Optional[MetadataProxy],
+ metadata: MetadataProxy | None,
response_metadata: MutableMapping,
routing_path: RoutingPath,
current_location: Location,
+ return_to: str | None = None,
+ return_to_block_id: str | None = None,
) -> None:
super().__init__(
language,
@@ -58,18 +49,30 @@ def __init__(
metadata,
response_metadata,
)
- self.routing_path = routing_path
+ self.routing_path_block_ids = routing_path.block_ids
self.current_location = current_location
+ self.return_to = return_to
+ self.return_to_block_id = return_to_block_id
def build_groups_for_section(
self,
- section: Mapping[str, Any],
+ *,
+ section: Mapping,
return_to_block_id: str,
- ) -> list[Mapping[str, Group]]:
+ routing_path_block_ids: Iterable[str],
+ ) -> list[Mapping]:
+ """
+ If the calculated summary is being edited from a grand calculated summary
+ the details of the grand calculated summary to return to needs to be passed down to the calculated summary answer links
+ """
+ return_to = "calculated-summary"
+ if self.return_to == "grand-calculated-summary":
+ return_to_block_id += f",{self.return_to_block_id}"
+ return_to += ",grand-calculated-summary"
return [
Group(
group_schema=group,
- routing_path=self.routing_path,
+ routing_path_block_ids=routing_path_block_ids,
answer_store=self._answer_store,
list_store=self._list_store,
metadata=self._metadata,
@@ -78,25 +81,25 @@ def build_groups_for_section(
location=self.current_location,
language=self._language,
progress_store=self._progress_store,
- return_to="calculated-summary",
+ return_to=return_to,
return_to_block_id=return_to_block_id,
+ summary_type="CalculatedSummary",
).serialize()
for group in section["groups"]
]
- def build_view_context_for_calculated_summary(self) -> dict[str, dict[str, Any]]:
+ def build_view_context(self) -> dict[str, dict]:
# type ignores added as block will exist at this point
block_id: str = self.current_location.block_id # type: ignore
block: ImmutableDict = self._schema.get_block(block_id) # type: ignore
- calculated_section: dict[str, Any] = self._build_calculated_summary_section(
- block
- )
+ calculated_section: dict = self._build_calculated_summary_section(block)
calculation = block["calculation"]
groups = self.build_groups_for_section(
- calculated_section,
- block_id,
+ section=calculated_section,
+ return_to_block_id=block_id,
+ routing_path_block_ids=self.routing_path_block_ids,
)
formatted_total = self._get_formatted_total(
@@ -108,6 +111,23 @@ def build_view_context_for_calculated_summary(self) -> dict[str, dict[str, Any]]
else calculation["operation"],
)
+ return self._build_formatted_summary(
+ block=block,
+ groups=groups,
+ calculation=calculation,
+ formatted_total=formatted_total,
+ summary_type="CalculatedSummary",
+ )
+
+ def _build_formatted_summary(
+ self,
+ *,
+ block: ImmutableDict,
+ groups: Iterable[Mapping],
+ calculation: ImmutableDict,
+ formatted_total: str,
+ summary_type: str,
+ ) -> dict[str, dict]:
collapsible = block.get("collapsible") or False
block_title = block["title"]
@@ -122,13 +142,11 @@ def build_view_context_for_calculated_summary(self) -> dict[str, dict[str, Any]]
),
"title": block_title % {"total": formatted_total},
"collapsible": collapsible,
- "summary_type": "CalculatedSummary",
+ "summary_type": summary_type,
}
}
- def _build_calculated_summary_section(
- self, rendered_block: ImmutableDict[str, Any]
- ) -> dict[str, Any]:
+ def _build_calculated_summary_section(self, rendered_block: ImmutableDict) -> dict:
"""Build up the list of blocks only including blocks / questions / answers which are relevant to the summary"""
# type ignores added as block will exist at this point
block_id: str = self.current_location.block_id # type: ignore
@@ -170,7 +188,7 @@ def _remove_unwanted_questions_answers(
"""
Evaluates questions in a block and removes any questions not containing a relevant answer
"""
- transformed_block: ImmutableDict = transform_variants(
+ block_to_transform: ImmutableDict = transform_variants(
block,
self._schema,
self._metadata,
@@ -180,8 +198,9 @@ def _remove_unwanted_questions_answers(
self.current_location,
self._progress_store,
)
- transformed_block = deepcopy(transformed_block)
- transformed_block = QuestionnaireSchema.get_mutable_deepcopy(transformed_block)
+ transformed_block: dict = QuestionnaireSchema.get_mutable_deepcopy(
+ block_to_transform
+ )
block_question = transformed_block["question"]
matching_answers = []
@@ -202,30 +221,45 @@ def _remove_unwanted_questions_answers(
return transformed_block
+ def _get_evaluated_total(
+ self,
+ *,
+ calculation: ImmutableDict,
+ routing_path_block_ids: Iterable[str],
+ ) -> NumericType:
+ """
+ For a calculation in the new style and the list of involved block ids (possibly across sections) evaluate the total
+ """
+ evaluate_calculated_summary = RuleEvaluator(
+ self._schema,
+ self._answer_store,
+ self._list_store,
+ self._metadata,
+ self._response_metadata,
+ routing_path_block_ids=routing_path_block_ids,
+ location=self.current_location,
+ progress_store=self._progress_store,
+ )
+ # Type ignore: in the case of a calculated summation it will always be a numeric type
+ calculated_total: NumericType = evaluate_calculated_summary.evaluate(calculation) # type: ignore
+ return calculated_total
+
def _get_formatted_total(
self, groups: list, calculation: Callable | ImmutableDict
) -> str:
answer_format, values_to_calculate = self._get_answer_format(groups)
if isinstance(calculation, ImmutableDict):
- evaluate_calculated_summary = RuleEvaluator(
- self._schema,
- self._answer_store,
- self._list_store,
- self._metadata,
- self._response_metadata,
- routing_path_block_ids=self.routing_path.block_ids,
- location=self.current_location,
- progress_store=self._progress_store,
+ calculated_total = self._get_evaluated_total(
+ calculation=calculation,
+ routing_path_block_ids=self.routing_path_block_ids,
)
-
- calculated_total: Union[int, float, Decimal] = evaluate_calculated_summary.evaluate(calculation) # type: ignore
else:
calculated_total = calculation(values_to_calculate)
- return self._format_total(answer_format, calculated_total)
+ return self._format_total(answer_format=answer_format, total=calculated_total)
- def _get_answer_format(self, groups: list) -> Tuple[dict[str, Any], list]:
+ def _get_answer_format(self, groups: Iterable[Mapping]) -> Tuple[dict, list]:
values_to_calculate: list = []
answer_format: dict = {"type": None}
for group in groups:
@@ -255,8 +289,9 @@ def _get_answer_format(self, groups: list) -> Tuple[dict[str, Any], list]:
@staticmethod
def _format_total(
+ *,
answer_format: Mapping[str, Literal["short", "long", "narrow"]],
- total: int | float | Decimal,
+ total: NumericType,
) -> str:
if answer_format["type"] == "currency":
return get_formatted_currency(total, answer_format["currency"])
@@ -275,9 +310,9 @@ def _format_total(
@staticmethod
def _get_calculated_question(
- calculation_question: ImmutableDict[str, Any],
+ calculation_question: ImmutableDict,
formatted_total: str,
- ) -> dict[str, Any]:
+ ) -> dict:
calculation_title = calculation_question["title"]
return {
diff --git a/app/views/contexts/grand_calculated_summary_context.py b/app/views/contexts/grand_calculated_summary_context.py
new file mode 100644
index 0000000000..467ba9f040
--- /dev/null
+++ b/app/views/contexts/grand_calculated_summary_context.py
@@ -0,0 +1,124 @@
+from typing import Iterable, Mapping
+
+from werkzeug.datastructures import ImmutableDict
+
+from app.questionnaire.questionnaire_schema import (
+ get_calculation_block_ids_for_grand_calculated_summary,
+)
+from app.views.contexts.calculated_summary_context import CalculatedSummaryContext
+from app.views.contexts.summary.group import Group
+
+
+class GrandCalculatedSummaryContext(CalculatedSummaryContext):
+ def _build_grand_calculated_summary_section(
+ self, rendered_block: ImmutableDict
+ ) -> dict[str, str | list]:
+ """
+ Build list of calculated summary blocks that the grand calculated summary will be adding up
+ """
+ # Type ignore: the block, group and section will all exist at this point
+ calculated_summary_group: ImmutableDict = self._schema.get_group_for_block_id(self.current_location.block_id) # type: ignore
+
+ calculated_summary_ids = get_calculation_block_ids_for_grand_calculated_summary(
+ rendered_block
+ )
+ blocks_to_calculate = [
+ self._schema.get_block(block_id) for block_id in calculated_summary_ids
+ ]
+
+ return {
+ "id": self.current_location.section_id,
+ "groups": [
+ {"id": calculated_summary_group["id"], "blocks": blocks_to_calculate}
+ ],
+ }
+
+ def _blocks_on_routing_path(
+ self, calculated_summary_ids: Iterable[str]
+ ) -> list[str]:
+ """
+ Find all blocks on the routing path for each of the calculated summaries
+ """
+ # Type ignore: each block must have a section id
+ section_ids: set[str] = {self._schema.get_section_id_for_block_id(block_id) for block_id in calculated_summary_ids} # type: ignore
+ # find any sections involved in the grand calculated summary (but only if they have started, to avoid evaluating the path if not necessary)
+ started_sections = [
+ key for key, _ in self._progress_store.started_section_keys(section_ids)
+ ]
+ routing_path_block_ids: list[str] = []
+
+ for section_id in started_sections:
+ if section_id == self.current_location.section_id:
+ routing_path_block_ids.extend(self.routing_path_block_ids)
+ else:
+ routing_path_block_ids.extend(
+ # repeating calculated summaries are not supported at the moment, so no list item is needed
+ self._router.routing_path(section_id).block_ids
+ )
+
+ return routing_path_block_ids
+
+ def build_groups_for_section(
+ self,
+ *,
+ section: Mapping,
+ return_to_block_id: str,
+ routing_path_block_ids: Iterable[str],
+ ) -> list[Mapping]:
+ return [
+ Group(
+ group_schema=group,
+ routing_path_block_ids=routing_path_block_ids,
+ answer_store=self._answer_store,
+ list_store=self._list_store,
+ metadata=self._metadata,
+ response_metadata=self._response_metadata,
+ schema=self._schema,
+ location=self.current_location,
+ language=self._language,
+ progress_store=self._progress_store,
+ return_to="grand-calculated-summary",
+ return_to_block_id=return_to_block_id,
+ summary_type="GrandCalculatedSummary",
+ ).serialize()
+ for group in section["groups"]
+ ]
+
+ def build_view_context(self) -> dict[str, dict]:
+ """
+ Build summary section with formatted total and change links for each calculated summary
+ """
+ # Type ignore: Block will exist at this point
+ block: ImmutableDict = self._schema.get_block(self.current_location.block_id) # type: ignore
+
+ calculation = block["calculation"]
+ calculated_summary_ids = get_calculation_block_ids_for_grand_calculated_summary(
+ block
+ )
+ routing_path_block_ids = self._blocks_on_routing_path(calculated_summary_ids)
+
+ calculated_section = self._build_grand_calculated_summary_section(block)
+
+ groups = self.build_groups_for_section(
+ section=calculated_section,
+ return_to_block_id=block["id"],
+ routing_path_block_ids=routing_path_block_ids,
+ )
+ total = self._get_evaluated_total(
+ calculation=calculation["operation"],
+ routing_path_block_ids=routing_path_block_ids,
+ )
+
+ # validator ensures all calculated summaries are of the same type, so the first can be used for the format
+ answer_format = self._schema.get_answer_format_for_calculated_summary(
+ calculated_summary_ids[0]
+ )
+ formatted_total = self._format_total(answer_format=answer_format, total=total)
+
+ return self._build_formatted_summary(
+ block=block,
+ groups=groups,
+ calculation=calculation,
+ formatted_total=formatted_total,
+ summary_type="GrandCalculatedSummary",
+ )
diff --git a/app/views/contexts/section_summary_context.py b/app/views/contexts/section_summary_context.py
index 30bebc684d..06c4fc392e 100644
--- a/app/views/contexts/section_summary_context.py
+++ b/app/views/contexts/section_summary_context.py
@@ -138,7 +138,7 @@ def build_summary(
"groups": [
Group(
group_schema=group,
- routing_path=self.routing_path,
+ routing_path_block_ids=self.routing_path.block_ids,
answer_store=self._answer_store,
list_store=self._list_store,
metadata=self._metadata,
@@ -172,7 +172,7 @@ def _custom_summary_elements(
for summary_element in section_summary:
if summary_element["type"] == "List":
list_collector_block = ListCollectorBlock(
- routing_path=self.routing_path,
+ routing_path_block_ids=self.routing_path.block_ids,
answer_store=self._answer_store,
list_store=self._list_store,
progress_store=self._progress_store,
diff --git a/app/views/contexts/summary/calculated_summary_block.py b/app/views/contexts/summary/calculated_summary_block.py
new file mode 100644
index 0000000000..8ba998fe74
--- /dev/null
+++ b/app/views/contexts/summary/calculated_summary_block.py
@@ -0,0 +1,83 @@
+from decimal import Decimal
+from typing import Iterable, Mapping, MutableMapping, TypeAlias
+
+from flask import url_for
+
+from app.data_models import AnswerStore, ListStore, ProgressStore
+from app.data_models.metadata_proxy import MetadataProxy
+from app.questionnaire import Location, QuestionnaireSchema
+from app.questionnaire.rules.rule_evaluator import RuleEvaluator
+
+NumericType: TypeAlias = int | float | Decimal
+
+
+class CalculatedSummaryBlock:
+ def __init__(
+ self,
+ block_schema: Mapping,
+ *,
+ answer_store: AnswerStore,
+ list_store: ListStore,
+ metadata: MetadataProxy | None,
+ response_metadata: MutableMapping,
+ schema: QuestionnaireSchema,
+ location: Location,
+ return_to: str | None,
+ return_to_block_id: str | None = None,
+ progress_store: ProgressStore,
+ routing_path_block_ids: Iterable[str],
+ ) -> None:
+ """
+ A Calculated summary block that is rendered as part of a grand calculated summary
+ """
+
+ self.id = block_schema["id"]
+ self.title = block_schema["calculation"]["title"]
+ self._return_to = return_to
+ self._return_to_block_id = return_to_block_id
+ self._block_schema = block_schema
+ self._schema = schema
+
+ self._rule_evaluator = RuleEvaluator(
+ schema=schema,
+ answer_store=answer_store,
+ list_store=list_store,
+ metadata=metadata,
+ response_metadata=response_metadata,
+ location=location,
+ progress_store=progress_store,
+ routing_path_block_ids=routing_path_block_ids,
+ )
+
+ # Type ignore: for a calculated summary the resolved answer would only ever be one of these 3
+ calculated_total: NumericType = self._rule_evaluator.evaluate(block_schema["calculation"]["operation"]) # type: ignore
+ answer_format = self._schema.get_answer_format_for_calculated_summary(self.id)
+ self.answers = [
+ {
+ "id": self.id,
+ "label": self.title,
+ "value": calculated_total,
+ "link": self._build_link(),
+ **answer_format,
+ }
+ ]
+
+ def _build_link(self) -> str:
+ return url_for(
+ "questionnaire.block",
+ block_id=self.id,
+ return_to=self._return_to,
+ return_to_answer_id=self.id,
+ return_to_block_id=self._return_to_block_id,
+ _anchor=self.id,
+ )
+
+ def _calculated_summary(self) -> dict:
+ return {"id": self.id, "title": self.title, "answers": self.answers}
+
+ def serialize(self) -> dict:
+ return {
+ "id": self.id,
+ "title": self.title,
+ "calculated_summary": self._calculated_summary(),
+ }
diff --git a/app/views/contexts/summary/group.py b/app/views/contexts/summary/group.py
index 9b4a15fae5..01be23b42c 100644
--- a/app/views/contexts/summary/group.py
+++ b/app/views/contexts/summary/group.py
@@ -1,4 +1,4 @@
-from typing import Any, Mapping, MutableMapping, Optional
+from typing import Iterable, Mapping, MutableMapping
from werkzeug.datastructures import ImmutableDict
@@ -6,9 +6,9 @@
from app.data_models.metadata_proxy import MetadataProxy
from app.questionnaire import Location, QuestionnaireSchema
from app.questionnaire.placeholder_renderer import PlaceholderRenderer
-from app.questionnaire.routing_path import RoutingPath
from app.survey_config.link import Link
from app.views.contexts.summary.block import Block
+from app.views.contexts.summary.calculated_summary_block import CalculatedSummaryBlock
from app.views.contexts.summary.list_collector_block import ListCollectorBlock
@@ -16,8 +16,8 @@ class Group:
def __init__(
self,
*,
- group_schema: Mapping[str, Any],
- routing_path: RoutingPath,
+ group_schema: Mapping,
+ routing_path_block_ids: Iterable[str],
answer_store: AnswerStore,
list_store: ListStore,
metadata: MetadataProxy | None,
@@ -28,6 +28,7 @@ def __init__(
progress_store: ProgressStore,
return_to: str | None,
return_to_block_id: str | None = None,
+ summary_type: str | None = None,
view_submitted_response: bool | None = False,
) -> None:
self.id = group_schema["id"]
@@ -39,7 +40,7 @@ def __init__(
self.blocks = self._build_blocks_and_links(
group_schema=group_schema,
- routing_path=routing_path,
+ routing_path_block_ids=routing_path_block_ids,
answer_store=answer_store,
list_store=list_store,
metadata=metadata,
@@ -51,6 +52,7 @@ def __init__(
language=language,
return_to_block_id=return_to_block_id,
view_submitted_response=view_submitted_response,
+ summary_type=summary_type,
)
self.placeholder_renderer = PlaceholderRenderer(
@@ -68,24 +70,25 @@ def __init__(
def _build_blocks_and_links(
self,
*,
- group_schema: Mapping[str, Any],
- routing_path: RoutingPath,
+ group_schema: Mapping,
+ routing_path_block_ids: Iterable[str],
answer_store: AnswerStore,
list_store: ListStore,
- metadata: Optional[MetadataProxy],
+ metadata: MetadataProxy | None,
response_metadata: MutableMapping,
schema: QuestionnaireSchema,
location: Location,
- return_to: Optional[str],
+ return_to: str | None,
progress_store: ProgressStore,
language: str,
- return_to_block_id: Optional[str],
+ return_to_block_id: str | None,
view_submitted_response: bool | None = False,
+ summary_type: str | None = None,
) -> list[dict[str, Block]]:
blocks = []
for block in group_schema["blocks"]:
- if block["id"] not in routing_path:
+ if block["id"] not in routing_path_block_ids:
continue
if block["type"] in [
"Question",
@@ -108,20 +111,38 @@ def _build_blocks_and_links(
).serialize()
]
)
+ # check the summary_type as opposed to the block type
+ # otherwise this gets called on section summaries as well
+ elif summary_type == "GrandCalculatedSummary":
+ blocks.extend(
+ [
+ CalculatedSummaryBlock(
+ block,
+ answer_store=answer_store,
+ list_store=list_store,
+ metadata=metadata,
+ response_metadata=response_metadata,
+ schema=schema,
+ location=location,
+ return_to=return_to,
+ return_to_block_id=return_to_block_id,
+ progress_store=progress_store,
+ routing_path_block_ids=routing_path_block_ids,
+ ).serialize()
+ ]
+ )
elif block["type"] == "ListCollector":
- section: Optional[ImmutableDict] = schema.get_section(
- location.section_id
- )
+ section: ImmutableDict | None = schema.get_section(location.section_id)
- summary_item: Optional[ImmutableDict]
+ summary_item: ImmutableDict | None
if summary_item := schema.get_summary_item_for_list_for_section(
# Type ignore: section id will not be optional at this point
section_id=section["id"], # type: ignore
list_name=block["for_list"],
):
list_collector_block = ListCollectorBlock(
- routing_path=routing_path,
+ routing_path_block_ids=routing_path_block_ids,
answer_store=answer_store,
list_store=list_store,
progress_store=progress_store,
@@ -149,7 +170,7 @@ def _build_blocks_and_links(
return blocks
- def serialize(self) -> Mapping[str, Any]:
+ def serialize(self) -> Mapping:
return self.placeholder_renderer.render(
data_to_render={
"id": self.id,
diff --git a/app/views/contexts/summary/list_collector_block.py b/app/views/contexts/summary/list_collector_block.py
index 93a4737f68..2a6a980757 100644
--- a/app/views/contexts/summary/list_collector_block.py
+++ b/app/views/contexts/summary/list_collector_block.py
@@ -1,5 +1,5 @@
from collections import defaultdict
-from typing import Any, Mapping, MutableMapping, Optional
+from typing import Any, Iterable, Mapping, MutableMapping, Optional
from flask import url_for
from werkzeug.datastructures import ImmutableDict
@@ -9,7 +9,6 @@
from app.data_models.metadata_proxy import MetadataProxy
from app.questionnaire import Location, QuestionnaireSchema
from app.questionnaire.placeholder_renderer import PlaceholderRenderer
-from app.questionnaire.routing_path import RoutingPath
from app.views.contexts.list_context import ListContext
from app.views.contexts.summary.block import Block
@@ -17,7 +16,7 @@
class ListCollectorBlock:
def __init__(
self,
- routing_path: RoutingPath,
+ routing_path_block_ids: Iterable[str],
answer_store: AnswerStore,
list_store: ListStore,
progress_store: ProgressStore,
@@ -47,7 +46,7 @@ def __init__(
self._answer_store = answer_store
self._metadata = metadata
self._response_metadata = response_metadata
- self._routing_path = routing_path
+ self._routing_path_block_ids = routing_path_block_ids
self._progress_store = progress_store
# pylint: disable=too-many-locals
@@ -74,7 +73,7 @@ def list_summary_element(self, summary: Mapping[str, Any]) -> dict[str, Any]:
list_collector_blocks_on_path = [
list_collector_block
for list_collector_block in list_collector_blocks
- if list_collector_block["id"] in self._routing_path.block_ids
+ if list_collector_block["id"] in self._routing_path_block_ids
]
list_collector_block = (
diff --git a/app/views/handlers/block.py b/app/views/handlers/block.py
index 6019bc762e..1495831744 100644
--- a/app/views/handlers/block.py
+++ b/app/views/handlers/block.py
@@ -53,6 +53,14 @@ def __init__(
def current_location(self):
return self._current_location
+ @property
+ def return_to(self):
+ return self._return_to
+
+ @property
+ def return_to_block_id(self):
+ return self._return_to_block_id
+
@cached_property
def questionnaire_store_updater(self):
return QuestionnaireStoreUpdater(
diff --git a/app/views/handlers/block_factory.py b/app/views/handlers/block_factory.py
index 91c0a3d6c3..7961432d15 100644
--- a/app/views/handlers/block_factory.py
+++ b/app/views/handlers/block_factory.py
@@ -4,7 +4,10 @@
from app.questionnaire import QuestionnaireSchema
from app.questionnaire.location import InvalidLocationException, Location
from app.questionnaire.relationship_location import RelationshipLocation
-from app.views.handlers.calculated_summary import CalculatedSummary
+from app.views.handlers.calculation_summary import (
+ CalculatedSummary,
+ GrandCalculatedSummary,
+)
from app.views.handlers.content import Content
from app.views.handlers.list_add_question import ListAddQuestion
from app.views.handlers.list_collector import ListCollector
@@ -30,6 +33,7 @@
"Introduction": Content,
"Interstitial": Content,
"CalculatedSummary": CalculatedSummary,
+ "GrandCalculatedSummary": GrandCalculatedSummary,
}
diff --git a/app/views/handlers/calculated_summary.py b/app/views/handlers/calculation_summary.py
similarity index 65%
rename from app/views/handlers/calculated_summary.py
rename to app/views/handlers/calculation_summary.py
index 325ada18e1..852ce7a3b2 100644
--- a/app/views/handlers/calculated_summary.py
+++ b/app/views/handlers/calculation_summary.py
@@ -1,10 +1,15 @@
+from typing import Type
+
+from app.views.contexts import GrandCalculatedSummaryContext
from app.views.contexts.calculated_summary_context import CalculatedSummaryContext
from app.views.handlers.content import Content
-class CalculatedSummary(Content):
+class _SummaryWithCalculation(Content):
+ summary_class: Type[CalculatedSummaryContext] | Type[GrandCalculatedSummaryContext]
+
def get_context(self):
- calculated_summary_context = CalculatedSummaryContext(
+ summary_context = self.summary_class(
language=self._language,
schema=self._schema,
answer_store=self._questionnaire_store.answer_store,
@@ -14,8 +19,10 @@ def get_context(self):
response_metadata=self._questionnaire_store.response_metadata,
current_location=self._current_location,
routing_path=self._routing_path,
+ return_to=self.return_to,
+ return_to_block_id=self.return_to_block_id,
)
- context = calculated_summary_context.build_view_context_for_calculated_summary()
+ context = summary_context.build_view_context()
if not self.page_title:
self.page_title = context["summary"]["calculated_question"]["title"]
@@ -28,3 +35,11 @@ def handle_post(self):
# Then we update dependent sections
self.questionnaire_store_updater.capture_progress_section_dependencies()
return super().handle_post()
+
+
+class CalculatedSummary(_SummaryWithCalculation):
+ summary_class = CalculatedSummaryContext
+
+
+class GrandCalculatedSummary(_SummaryWithCalculation):
+ summary_class = GrandCalculatedSummaryContext
diff --git a/schemas/test/en/test_grand_calculated_summary.json b/schemas/test/en/test_grand_calculated_summary.json
new file mode 100644
index 0000000000..e2271d03b9
--- /dev/null
+++ b/schemas/test/en/test_grand_calculated_summary.json
@@ -0,0 +1,294 @@
+{
+ "mime_type": "application/json/ons/eq",
+ "language": "en",
+ "schema_version": "0.0.1",
+ "data_version": "0.0.3",
+ "survey_id": "0",
+ "title": "Simple Grand Calculated Summary demo",
+ "theme": "default",
+ "description": "A schema to showcase Grand Calculated Summary.",
+ "metadata": [
+ {
+ "name": "user_id",
+ "type": "string"
+ },
+ {
+ "name": "period_id",
+ "type": "string"
+ },
+ {
+ "name": "ru_name",
+ "type": "string"
+ }
+ ],
+ "questionnaire_flow": {
+ "type": "Linear",
+ "options": {
+ "summary": {
+ "collapsible": false
+ }
+ }
+ },
+ "sections": [
+ {
+ "id": "section-1",
+ "title": "Commuting",
+ "groups": [
+ {
+ "id": "group",
+ "title": "Commuting",
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "first-number-block",
+ "question": {
+ "id": "first-number-question",
+ "title": "How much do you walk per week?",
+ "type": "General",
+ "answers": [
+ {
+ "id": "q1-a1",
+ "label": "Weekly distance travelled on foot",
+ "mandatory": true,
+ "type": "Unit",
+ "unit_length": "short",
+ "unit": "length-mile",
+ "decimal_places": 2
+ },
+ {
+ "id": "q1-a2",
+ "label": "Number of walks per week",
+ "mandatory": true,
+ "type": "Number"
+ }
+ ]
+ }
+ },
+ {
+ "type": "Question",
+ "id": "second-number-block",
+ "question": {
+ "id": "second-number-question",
+ "title": "How much do you drive per week?",
+ "type": "General",
+ "answers": [
+ {
+ "id": "q2-a1",
+ "label": "Weekly distance travelled by car",
+ "mandatory": true,
+ "type": "Unit",
+ "unit_length": "short",
+ "unit": "length-mile",
+ "decimal_places": 2
+ },
+ {
+ "id": "q2-a2",
+ "label": "Number of car journeys per week",
+ "mandatory": true,
+ "type": "Number"
+ }
+ ]
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "distance-calculated-summary-1",
+ "title": "We calculate the total of distance travelled by foot and car to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q1-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a1"
+ }
+ ]
+ },
+ "title": "Calculated distance on foot and driving"
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "number-calculated-summary-1",
+ "title": "We calculate the total number of journeys on foot and in a car to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q1-a2"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a2"
+ }
+ ]
+ },
+ "title": "Calculated journeys on foot and driving"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "section-2",
+ "title": "Alternative Transport",
+ "groups": [
+ {
+ "id": "transport-group",
+ "title": "Alternative Transport",
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "third-number-block",
+ "question": {
+ "id": "third-number-question",
+ "title": "How much do you cycle per week?",
+ "type": "General",
+ "answers": [
+ {
+ "id": "q3-a1",
+ "label": "Weekly distance travelled by bike",
+ "mandatory": true,
+ "type": "Unit",
+ "unit_length": "short",
+ "unit": "length-mile",
+ "decimal_places": 2
+ },
+ {
+ "id": "q3-a2",
+ "label": "Number of bicycle journeys per week",
+ "mandatory": true,
+ "type": "Number"
+ }
+ ]
+ }
+ },
+ {
+ "type": "Question",
+ "id": "fourth-number-block",
+ "question": {
+ "id": "fourth-number-question",
+ "title": "How much do you voi per week?",
+ "type": "General",
+ "answers": [
+ {
+ "id": "q4-a1",
+ "label": "Weekly distance travelled on a Voi",
+ "mandatory": true,
+ "type": "Unit",
+ "unit_length": "short",
+ "unit": "length-mile",
+ "decimal_places": 2
+ },
+ {
+ "id": "q4-a2",
+ "label": "Number of scooter trips per week",
+ "mandatory": true,
+ "type": "Number"
+ }
+ ]
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "distance-calculated-summary-2",
+ "title": "We calculate the total of distance travelled by bike and voi to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q3-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q4-a1"
+ }
+ ]
+ },
+ "title": "Calculated weekly distance on bike and scooter"
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "number-calculated-summary-2",
+ "title": "We calculate the total number of journeys on bike and on a voi to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q3-a2"
+ },
+ {
+ "source": "answers",
+ "identifier": "q4-a2"
+ }
+ ]
+ },
+ "title": "Calculated journeys on bike and scooter"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "section-3",
+ "title": "Grand calculated summaries",
+ "groups": [
+ {
+ "id": "summary-group",
+ "title": "Grand calculated summary group",
+ "blocks": [
+ {
+ "type": "GrandCalculatedSummary",
+ "id": "distance-grand-calculated-summary",
+ "title": "We calculate the grand total weekly distance travelled to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "calculated_summary",
+ "identifier": "distance-calculated-summary-1"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "distance-calculated-summary-2"
+ }
+ ]
+ },
+ "title": "Grand calculated summary of distance travelled"
+ }
+ },
+ {
+ "type": "GrandCalculatedSummary",
+ "id": "number-grand-calculated-summary",
+ "title": "We calculate the grand total journeys per week to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "calculated_summary",
+ "identifier": "number-calculated-summary-1"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "number-calculated-summary-2"
+ }
+ ]
+ },
+ "title": "Grand calculated summary of journeys"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/schemas/test/en/test_grand_calculated_summary_cross_section_dependencies.json b/schemas/test/en/test_grand_calculated_summary_cross_section_dependencies.json
new file mode 100644
index 0000000000..b0754d04b6
--- /dev/null
+++ b/schemas/test/en/test_grand_calculated_summary_cross_section_dependencies.json
@@ -0,0 +1,341 @@
+{
+ "mime_type": "application/json/ons/eq",
+ "language": "en",
+ "schema_version": "0.0.1",
+ "data_version": "0.0.3",
+ "survey_id": "0",
+ "title": "Grand Calculated Summary Cross Section Dependencies",
+ "theme": "default",
+ "description": "A questionnaire to demo resolution of grand calculated summary values across sections",
+ "metadata": [
+ {
+ "name": "user_id",
+ "type": "string"
+ },
+ {
+ "name": "period_id",
+ "type": "string"
+ },
+ {
+ "name": "ru_name",
+ "type": "string"
+ }
+ ],
+ "questionnaire_flow": {
+ "type": "Hub",
+ "options": {}
+ },
+ "sections": [
+ {
+ "id": "questions-section",
+ "title": "Household bills",
+ "summary": {
+ "show_on_completion": true
+ },
+ "groups": [
+ {
+ "id": "radio",
+ "title": "Questions",
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "skip-first-block",
+ "question": {
+ "type": "General",
+ "id": "skip-question-1",
+ "title": "Are you a student?",
+ "guidance": {
+ "contents": [
+ {
+ "description": "If you answer yes, then the question about council tax will be skipped and not included in total monthly expenditure."
+ }
+ ]
+ },
+ "answers": [
+ {
+ "type": "Radio",
+ "id": "skip-answer-1",
+ "mandatory": false,
+ "options": [
+ {
+ "label": "Yes",
+ "value": "Yes"
+ },
+ {
+ "label": "No",
+ "value": "No"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "skip_conditions": {
+ "when": {
+ "==": [
+ {
+ "identifier": "skip-answer-1",
+ "source": "answers"
+ },
+ "Yes"
+ ]
+ }
+ },
+ "type": "Question",
+ "id": "first-number-block-part-a",
+ "question": {
+ "id": "question-1-a",
+ "title": "How much do you pay monthly for council tax?",
+ "type": "General",
+ "answers": [
+ {
+ "id": "first-number-answer-a",
+ "label": "Council tax (optional)",
+ "mandatory": false,
+ "type": "Currency",
+ "currency": "GBP",
+ "decimal_places": 2
+ }
+ ]
+ }
+ },
+ {
+ "type": "Question",
+ "id": "second-number-block",
+ "question": {
+ "id": "question-2",
+ "title": "How much are your monthly gas, water and electricity bills?",
+ "type": "General",
+ "answers": [
+ {
+ "id": "second-number-answer-a",
+ "label": "Electricity Bill",
+ "mandatory": false,
+ "type": "Currency",
+ "currency": "GBP",
+ "decimal_places": 2
+ },
+ {
+ "id": "second-number-answer-b",
+ "label": "Gas Bill",
+ "mandatory": false,
+ "type": "Currency",
+ "currency": "GBP",
+ "decimal_places": 2
+ },
+ {
+ "id": "second-number-answer-c",
+ "label": "Water Bill",
+ "mandatory": false,
+ "type": "Currency",
+ "currency": "GBP",
+ "decimal_places": 2
+ }
+ ]
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "currency-section-1",
+ "title": "We calculate your total monthly expenditure on household bills to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "second-number-answer-a"
+ },
+ {
+ "source": "answers",
+ "identifier": "second-number-answer-b"
+ },
+ {
+ "source": "answers",
+ "identifier": "second-number-answer-c"
+ },
+ {
+ "source": "answers",
+ "identifier": "first-number-answer-a"
+ }
+ ]
+ },
+ "title": "Monthly expenditure on household bills"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "calculated-summary-section",
+ "title": "Other outgoing costs",
+ "summary": {
+ "show_on_completion": true
+ },
+ "groups": [
+ {
+ "id": "calculated-summary",
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "third-number-block",
+ "question": {
+ "id": "third-number-question",
+ "title": "How much do you spend on internet and television?",
+ "type": "General",
+ "guidance": {
+ "contents": [
+ {
+ "description": "If you enter a value for the TV licence, it will unlock an additional question about premium channels."
+ }
+ ]
+ },
+ "answers": [
+ {
+ "id": "third-number-answer-part-a",
+ "label": "Internet bill",
+ "mandatory": true,
+ "type": "Currency",
+ "currency": "GBP",
+ "decimal_places": 2
+ },
+ {
+ "id": "third-number-answer-part-b",
+ "label": "TV licence (optional)",
+ "mandatory": false,
+ "type": "Currency",
+ "currency": "GBP",
+ "decimal_places": 2
+ }
+ ]
+ }
+ },
+ {
+ "skip_conditions": {
+ "when": {
+ "==": [
+ {
+ "identifier": "third-number-answer-part-b",
+ "source": "answers"
+ },
+ null
+ ]
+ }
+ },
+ "type": "Question",
+ "id": "fourth-number-block",
+ "question": {
+ "id": "fourth-number-question",
+ "title": "How much do you spend per month on premium television channels?",
+ "type": "General",
+ "answers": [
+ {
+ "id": "fourth-number-answer",
+ "label": "TV channel subscription fees",
+ "mandatory": true,
+ "type": "Currency",
+ "currency": "GBP",
+ "decimal_places": 2
+ }
+ ]
+ }
+ },
+ {
+ "type": "Question",
+ "id": "skip-calculated-summary",
+ "question": {
+ "type": "General",
+ "id": "skip-question-2",
+ "title": "Skip the calculated summary of other outgoing costs so it isn’t included in the grand calculated summary?",
+ "answers": [
+ {
+ "type": "Radio",
+ "id": "skip-answer-2",
+ "mandatory": false,
+ "options": [
+ {
+ "label": "Yes",
+ "value": "Yes"
+ },
+ {
+ "label": "No",
+ "value": "No"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "skip_conditions": {
+ "when": {
+ "==": [
+ {
+ "identifier": "skip-answer-2",
+ "source": "answers"
+ },
+ "Yes"
+ ]
+ }
+ },
+ "type": "CalculatedSummary",
+ "id": "currency-question-3",
+ "title": "We calculate the total monthly spending on internet and TV to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "third-number-answer-part-a"
+ },
+ {
+ "source": "answers",
+ "identifier": "third-number-answer-part-b"
+ },
+ {
+ "source": "answers",
+ "identifier": "fourth-number-answer"
+ }
+ ]
+ },
+ "title": "Total monthly spending on internet and TV"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "grand-calculated-summary-section",
+ "title": "Grand Calculated Summary",
+ "groups": [
+ {
+ "id": "grand-calculated-summary",
+ "blocks": [
+ {
+ "type": "GrandCalculatedSummary",
+ "id": "currency-all",
+ "title": "The grand calculated summary is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "operation": {
+ "+": [
+ {
+ "source": "calculated_summary",
+ "identifier": "currency-section-1"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "currency-question-3"
+ }
+ ]
+ },
+ "title": "Grand total monthly expenditure"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/schemas/test/en/test_grand_calculated_summary_multiple_sections.json b/schemas/test/en/test_grand_calculated_summary_multiple_sections.json
new file mode 100644
index 0000000000..8b7700df1d
--- /dev/null
+++ b/schemas/test/en/test_grand_calculated_summary_multiple_sections.json
@@ -0,0 +1,348 @@
+{
+ "mime_type": "application/json/ons/eq",
+ "language": "en",
+ "schema_version": "0.0.1",
+ "data_version": "0.0.3",
+ "survey_id": "0",
+ "title": "Grand Calculated Summary cross section demo",
+ "theme": "default",
+ "description": "A schema to showcase grand calculated summaries across multiple sections and groups",
+ "metadata": [
+ {
+ "name": "user_id",
+ "type": "string"
+ },
+ {
+ "name": "period_id",
+ "type": "string"
+ },
+ {
+ "name": "ru_name",
+ "type": "string"
+ }
+ ],
+ "questionnaire_flow": {
+ "type": "Hub",
+ "options": {}
+ },
+ "sections": [
+ {
+ "id": "section-1",
+ "title": "Section 1",
+ "summary": {
+ "show_on_completion": true
+ },
+ "groups": [
+ {
+ "id": "group-1",
+ "title": "Group title for questions about food",
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "block-1",
+ "question": {
+ "type": "General",
+ "id": "question-1",
+ "title": "How much do you spend per week on fruit and veg? (Question 1, Group 1, Section 1)",
+ "answers": [
+ {
+ "id": "q1-a1",
+ "label": "Money spent on fruit",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ },
+ {
+ "id": "q1-a2",
+ "label": "Money spent on veg",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ }
+ ]
+ }
+ },
+ {
+ "type": "Question",
+ "id": "block-2",
+ "question": {
+ "type": "General",
+ "id": "question-2",
+ "title": "How much do you spend per week on other food? (Question 2, Group 1, Section 1)",
+ "answers": [
+ {
+ "id": "q2-a1",
+ "label": "Money spent on bread",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ },
+ {
+ "id": "q2-a2",
+ "label": "Money spent on not bread",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ }
+ ]
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "calculated-summary-1",
+ "title": "Calculated Summary for Question 1 is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "title": "Total food expenditure",
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q1-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q1-a2"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a2"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "id": "group-2",
+ "title": "Group title for clothing questions",
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "block-3",
+ "question": {
+ "type": "General",
+ "id": "question-3",
+ "title": "How much do you spend per week on clothes? (Question 3, Group 2, Section 1)",
+ "answers": [
+ {
+ "id": "q3-a1",
+ "label": "Money spent on jumpers",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ },
+ {
+ "id": "q3-a2",
+ "label": "Money spent on hats",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ }
+ ]
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "calculated-summary-2",
+ "title": "Calculated summary for question 3 is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "title": "Total clothes expenditure",
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q3-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q3-a2"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "calculated-summary-3",
+ "title": "Calculated summary for section 1 is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "title": "Total food and clothes expenditure",
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q1-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q1-a2"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a2"
+ },
+ {
+ "source": "answers",
+ "identifier": "q3-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q3-a2"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "GrandCalculatedSummary",
+ "id": "grand-calculated-summary-1",
+ "title": "Grand Calculated Summary which should match the previous calculated summary is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "title": "Grand calculated summary",
+ "operation": {
+ "+": [
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-1"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-2"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "section-2",
+ "title": "Section 2",
+ "groups": [
+ {
+ "id": "group-3",
+ "title": "Group title for questions about games",
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "block-4",
+ "question": {
+ "type": "General",
+ "id": "question-4",
+ "title": "How much do you spend per week on games? (Question 4, section 2, group 3)",
+ "guidance": {
+ "contents": [
+ {
+ "description": "Note:"
+ },
+ {
+ "list": [
+ "The grand calculated summary section after this will only show if the total spending on games is not zero",
+ "You should test that if you use the change links on the grand calculated summary to come back here and set both answers to 0, that you are not routed to the grand calculated summary when you press continue on the calculated summary, but instead, taken to the Hub.",
+ "If you use the change links on the grand calculated summary to edit these answer values to a non-zero sum, pressing continue twice should take you back to the grand calculated summary"
+ ]
+ }
+ ]
+ },
+ "answers": [
+ {
+ "id": "q4-a1",
+ "label": "Video games",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ },
+ {
+ "id": "q4-a2",
+ "label": "Board games",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ }
+ ]
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "calculated-summary-4",
+ "title": "Calculated Summary for Question 4 is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "title": "Total games expenditure",
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q4-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q4-a2"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "section-3",
+ "title": "Grand calculated summary section which only shows if the previous calculated summary is non zero",
+ "enabled": {
+ "when": {
+ "!=": [
+ 0,
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-4"
+ }
+ ]
+ }
+ },
+ "groups": [
+ {
+ "id": "group-4",
+ "title": "Group title for the grand calculated summary of both sections",
+ "blocks": [
+ {
+ "type": "GrandCalculatedSummary",
+ "id": "grand-calculated-summary-2",
+ "title": "Grand Calculated Summary for section 1 and 2 is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "title": "Total food clothes and games expenditure",
+ "operation": {
+ "+": [
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-1"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-2"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-4"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/schemas/test/en/test_grand_calculated_summary_overlapping_answers.json b/schemas/test/en/test_grand_calculated_summary_overlapping_answers.json
new file mode 100644
index 0000000000..78bd81e4b0
--- /dev/null
+++ b/schemas/test/en/test_grand_calculated_summary_overlapping_answers.json
@@ -0,0 +1,329 @@
+{
+ "mime_type": "application/json/ons/eq",
+ "language": "en",
+ "schema_version": "0.0.1",
+ "data_version": "0.0.3",
+ "survey_id": "0",
+ "title": "Grand Calculated Summary with overlapping answers",
+ "theme": "default",
+ "description": "A schema to showcase grand calculated summaries which include multiple calculated summaries using the same answers.",
+ "metadata": [
+ {
+ "name": "user_id",
+ "type": "string"
+ },
+ {
+ "name": "period_id",
+ "type": "string"
+ },
+ {
+ "name": "ru_name",
+ "type": "string"
+ }
+ ],
+ "questionnaire_flow": {
+ "type": "Hub",
+ "options": {
+ "required_completed_sections": ["introduction-section"]
+ }
+ },
+ "sections": [
+ {
+ "id": "introduction-section",
+ "title": "Introduction",
+ "show_on_hub": false,
+ "groups": [
+ {
+ "id": "introduction-group",
+ "title": "Introduction",
+ "blocks": [
+ {
+ "type": "Introduction",
+ "id": "introduction-block",
+ "primary_content": [
+ {
+ "id": "about",
+ "contents": [
+ {
+ "title": "About",
+ "list": [
+ "This survey tests that when you re-use answers between calculated summaries, the grand calculated summary still resolves to the correct value"
+ ]
+ },
+ {
+ "title": "How to test this schema",
+ "list": [
+ "Ensure that the grand calculated summary section does not show unless all dependent calculated summaries in section-1 have been confirmed.",
+ "Your answer to the third question, may unlock an additional calculated summary which re-use your answers to the first two questions",
+ "If you do not select to buy extra food, verify no additional calculated summary occurs, and that the grand calculated summary is correct",
+ "If you choose to buy any food items twice, verify that they are included twice in the grand calculated summary, one for each calculated summary",
+ "Verify that if you have the extra calculated summary, and change the cost of bread for example using either of the calculated summary change links which include it that you are routed to each calculated summary first, and only then the grand calculated summary"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "section-1",
+ "title": "Weekly shop",
+ "summary": {
+ "show_on_completion": true
+ },
+ "groups": [
+ {
+ "id": "group-1",
+ "title": "Weekly shopping",
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "block-1",
+ "question": {
+ "type": "General",
+ "id": "question-1",
+ "title": "How much do you spend on the following in a typical weekly shop?",
+ "answers": [
+ {
+ "id": "q1-a1",
+ "label": "Money on milk",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ },
+ {
+ "id": "q1-a2",
+ "label": "Money on eggs",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ }
+ ]
+ }
+ },
+ {
+ "type": "Question",
+ "id": "block-2",
+ "question": {
+ "type": "General",
+ "id": "question-2",
+ "title": "How much do you spend on these items in a typical week?",
+ "answers": [
+ {
+ "id": "q2-a1",
+ "label": "Money on bread",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ },
+ {
+ "id": "q2-a2",
+ "label": "Money on cheese",
+ "type": "Currency",
+ "currency": "GBP",
+ "mandatory": false
+ }
+ ]
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "calculated-summary-1",
+ "title": "Total of milk and bread is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "title": "milk + bread",
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q1-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a1"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "CalculatedSummary",
+ "id": "calculated-summary-2",
+ "title": "Total of eggs and cheese is calculated to be %(total)s. Is this correct?",
+ "calculation": {
+ "title": "eggs + cheese",
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q1-a2"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a2"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "Question",
+ "id": "block-3",
+ "question": {
+ "type": "General",
+ "id": "question-3",
+ "title": "Do you want to buy extra of anything this week?",
+ "guidance": {
+ "contents": [
+ {
+ "description": "If you select the first option, all your answers so far will be reused in a new calculated summary for extra shopping. If you select the second option, only your answers for bread and cheese will be reused."
+ }
+ ]
+ },
+ "answers": [
+ {
+ "type": "Radio",
+ "id": "radio-extra",
+ "mandatory": true,
+ "options": [
+ {
+ "label": "Yes, I am going to buy two of everything",
+ "value": "Yes, I am going to buy two of everything"
+ },
+ {
+ "label": "Yes, extra bread and cheese",
+ "value": "Yes, extra bread and cheese"
+ },
+ {
+ "label": "No",
+ "value": "No"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ "skip_conditions": {
+ "when": {
+ "!=": [
+ {
+ "source": "answers",
+ "identifier": "radio-extra"
+ },
+ "Yes, I am going to buy two of everything"
+ ]
+ }
+ },
+ "type": "CalculatedSummary",
+ "id": "calculated-summary-3",
+ "title": "Total extra items purchased is calculated to be %(total)s. Is this correct? This reuses your answers to question 1 and 2",
+ "calculation": {
+ "title": "(extra) milk + eggs + bread + cheese",
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q1-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q1-a2"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a2"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "skip_conditions": {
+ "when": {
+ "!=": [
+ {
+ "source": "answers",
+ "identifier": "radio-extra"
+ },
+ "Yes, extra bread and cheese"
+ ]
+ }
+ },
+ "type": "CalculatedSummary",
+ "id": "calculated-summary-4",
+ "title": "Total extra items cost is calculated to be %(total)s. Is this correct? This is reusing your bread and cheese answers",
+ "calculation": {
+ "title": "(extra) bread + cheese",
+ "operation": {
+ "+": [
+ {
+ "source": "answers",
+ "identifier": "q2-a1"
+ },
+ {
+ "source": "answers",
+ "identifier": "q2-a2"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "section-3",
+ "title": "Grand calculated summary",
+ "enabled": {
+ "when": {
+ "==": [{ "source": "progress", "selector": "section", "identifier": "section-1" }, "COMPLETED"]
+ }
+ },
+ "groups": [
+ {
+ "id": "group-2",
+ "title": "Grand calculated summary",
+ "blocks": [
+ {
+ "type": "GrandCalculatedSummary",
+ "id": "grand-calculated-summary-shopping",
+ "title": "Grand Calculated Summary of purchases this week comes to %(total)s. Is this correct?.",
+ "calculation": {
+ "title": "Weekly shopping cost",
+ "operation": {
+ "+": [
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-1"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-2"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-3"
+ },
+ {
+ "source": "calculated_summary",
+ "identifier": "calculated-summary-4"
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/templates/calculatedsummary.html b/templates/calculatedsummary.html
index 674524fea9..bdb0f96511 100644
--- a/templates/calculatedsummary.html
+++ b/templates/calculatedsummary.html
@@ -1,34 +1,5 @@
-{% extends 'layouts/_questionnaire.html' %}
-{% from "components/button/_macro.njk" import onsButton %}
-{% from "components/panel/_macro.njk" import onsPanel %}
+{% extends 'layouts/_calculatedsummary.html' %}
-{% set save_on_signout = true %}
-
-{% block form_content %}
- {{content.summary.title}}
-
- {% call onsPanel({
- "classes": "ons-u-mb-l"
- }) %}
- {{ _("Please review your answers and confirm these are correct") }}
- {% endcall %}
-
-
- {% include 'partials/summary/summary.html' %}
-
-{% endblock -%}
-
-{% block submit_button %}
- {{
- onsButton({
- "text": _("Yes, I confirm these are correct"),
- "variants": 'timer',
- "attributes": {
- "data-qa": "btn-submit",
- "data-ga-category": "Submit button",
- "data-ga-action": "Confirm",
- "data-ga-label": "Confirm button click"
- }
- })
- }}
+{% block form_title %}
+ {{ content.summary.title }}
{% endblock %}
diff --git a/templates/grandcalculatedsummary.html b/templates/grandcalculatedsummary.html
new file mode 100644
index 0000000000..875870974a
--- /dev/null
+++ b/templates/grandcalculatedsummary.html
@@ -0,0 +1,5 @@
+{% extends 'layouts/_calculatedsummary.html' %}
+
+{% block form_title %}
+ {{ content.summary.title }}
+{% endblock %}
diff --git a/templates/layouts/_calculatedsummary.html b/templates/layouts/_calculatedsummary.html
new file mode 100644
index 0000000000..b618e817b3
--- /dev/null
+++ b/templates/layouts/_calculatedsummary.html
@@ -0,0 +1,35 @@
+{% extends 'layouts/_questionnaire.html' %}
+{% from "components/button/_macro.njk" import onsButton %}
+{% from "components/panel/_macro.njk" import onsPanel %}
+
+{% set save_on_signout = true %}
+
+{% block form_content %}
+ {% block form_title %}
+ {% endblock %}
+
+ {% call onsPanel({
+ "classes": "ons-u-mb-l"
+ }) %}
+ {{ _("Please review your answers and confirm these are correct") }}
+ {% endcall %}
+
+
+ {% include 'partials/summary/summary.html' %}
+
+{% endblock -%}
+
+{% block submit_button %}
+ {{
+ onsButton({
+ "text": _("Yes, I confirm these are correct"),
+ "variants": 'timer',
+ "attributes": {
+ "data-qa": "btn-submit",
+ "data-ga-category": "Submit button",
+ "data-ga-action": "Confirm",
+ "data-ga-label": "Confirm button click"
+ }
+ })
+ }}
+{% endblock %}
diff --git a/tests/app/questionnaire/conftest.py b/tests/app/questionnaire/conftest.py
index 876b236b79..5e92c71cd0 100644
--- a/tests/app/questionnaire/conftest.py
+++ b/tests/app/questionnaire/conftest.py
@@ -6,7 +6,7 @@
from app.data_models.answer_store import Answer, 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.progress_store import CompletionStatus, ProgressStore
from app.questionnaire import QuestionnaireSchema
from app.questionnaire.location import Location
from app.questionnaire.placeholder_parser import PlaceholderParser
@@ -1311,6 +1311,29 @@ def progress_dependencies_schema():
)
+@pytest.fixture
+def grand_calculated_summary_schema():
+ return load_schema_from_name("test_grand_calculated_summary")
+
+
+@pytest.fixture
+def grand_calculated_summary_progress_store():
+ return ProgressStore(
+ [
+ {
+ "section_id": "section-1",
+ "block_ids": [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ "number-calculated-summary-1",
+ ],
+ "status": CompletionStatus.COMPLETED,
+ }
+ ]
+ )
+
+
@pytest.fixture
@pytest.mark.usefixtures("app", "gb_locale")
def placeholder_transform_question_dynamic_answers_json():
diff --git a/tests/app/questionnaire/test_router.py b/tests/app/questionnaire/test_router.py
index 2772e5ad09..7c1ec52e6f 100644
--- a/tests/app/questionnaire/test_router.py
+++ b/tests/app/questionnaire/test_router.py
@@ -510,7 +510,25 @@ def test_section_summary_on_completion_false(self):
("test_calculated_summary",),
)
def test_return_to_calculated_summary(self, schema):
+ """
+ This tests that when you hit continue on an edited answer for a calculated summary
+ and all dependent answers for that calculated summary are complete, you are routed to the calculated summary
+ """
self.schema = load_schema_from_name(schema)
+ # for the purposes of this test, assume the routing path consists only of the first two blocks and the calculated summary
+ # and that those two blocks are complete - this will be a sufficient condition to return to the calculated summary
+ self.progress_store = ProgressStore(
+ [
+ {
+ "section_id": "default-section",
+ "block_ids": [
+ "first-number-block",
+ "second-number-block",
+ ],
+ "status": CompletionStatus.IN_PROGRESS,
+ }
+ ]
+ )
current_location = Location(
section_id="default-section", block_id="second-number-block"
@@ -518,8 +536,9 @@ def test_return_to_calculated_summary(self, schema):
routing_path = RoutingPath(
[
+ "first-number-block",
"second-number-block",
- "currency-total-playback-skipped-fourth",
+ "currency-total-playback",
],
section_id="default-section",
)
@@ -529,11 +548,11 @@ def test_return_to_calculated_summary(self, schema):
routing_path,
return_to_answer_id="first-number-answer",
return_to="calculated-summary",
- return_to_block_id="currency-total-playback-skipped-fourth",
+ return_to_block_id="currency-total-playback",
)
expected_location = Location(
section_id="default-section",
- block_id="currency-total-playback-skipped-fourth",
+ block_id="currency-total-playback",
)
expected_location_url = url_for(
@@ -541,8 +560,7 @@ def test_return_to_calculated_summary(self, schema):
list_item_id=expected_location.list_item_id,
block_id=expected_location.block_id,
return_to="calculated-summary",
- return_to_answer_id="first-number-answer",
- return_to_block_id="currency-total-playback-skipped-fourth",
+ _anchor="first-number-answer",
)
assert expected_location_url == next_location_url
@@ -556,10 +574,24 @@ def test_return_to_calculated_summary(self, schema):
),
)
def test_return_to_calculated_summary_not_on_allowable_path(self, schema):
+ """
+ This tests that if you try to return to a calculated summary before all its dependencies have been answered
+ then you are instead routed to the first incomplete block of the section
+ """
self.schema = load_schema_from_name(schema)
+ self.progress_store = ProgressStore(
+ [
+ {
+ "section_id": "default-section",
+ "block_ids": ["block-3"],
+ "status": CompletionStatus.IN_PROGRESS,
+ }
+ ]
+ )
current_location = Location(section_id="default-section", block_id="block-3")
+ # block 3 is complete, and block 4 is not, so block 4 should be routed to before the calculated summary
routing_path = RoutingPath(
[
"block-3",
@@ -587,7 +619,6 @@ def test_return_to_calculated_summary_not_on_allowable_path(self, schema):
list_item_id=expected_location.list_item_id,
block_id=expected_location.block_id,
return_to="calculated-summary",
- return_to_answer_id="answer-3",
return_to_block_id="calculated-summary-block",
)
@@ -613,6 +644,15 @@ def test_return_to_calculated_summary_invalid_return_to_block_id(
self, schema, return_to_block_id, expected_url
):
self.schema = load_schema_from_name(schema)
+ self.progress_store = ProgressStore(
+ [
+ {
+ "section_id": "default-section",
+ "block_ids": ["fifth-number-block"],
+ "status": CompletionStatus.IN_PROGRESS,
+ }
+ ]
+ )
current_location = Location(
section_id="default-section", block_id="fifth-number-block"
@@ -638,6 +678,15 @@ def test_return_to_calculated_summary_invalid_return_to_block_id(
)
def test_return_to_calculated_summary_return_to_block_id_not_on_path(self, schema):
self.schema = load_schema_from_name(schema)
+ self.progress_store = ProgressStore(
+ [
+ {
+ "section_id": "default-section",
+ "block_ids": ["fifth-number-block"],
+ "status": CompletionStatus.IN_PROGRESS,
+ }
+ ]
+ )
current_location = Location(
section_id="default-section", block_id="fifth-number-block"
@@ -661,6 +710,196 @@ def test_return_to_calculated_summary_return_to_block_id_not_on_path(self, schem
== next_location_url
)
+ @pytest.mark.usefixtures("app")
+ def test_return_to_grand_calculated_summary_from_answer(
+ self, grand_calculated_summary_progress_store, grand_calculated_summary_schema
+ ):
+ """
+ If going from GCS -> CS -> answer -> CS -> GCS this tests going from CS -> GCS having just come from an answer
+ """
+ self.schema = grand_calculated_summary_schema
+ self.progress_store = grand_calculated_summary_progress_store
+
+ current_location = Location(
+ section_id="section-1", block_id="first-number-block"
+ )
+
+ routing_path = RoutingPath(
+ ["distance-calculated-summary-1"],
+ section_id="section-1",
+ )
+ next_location_url = self.router.get_next_location_url(
+ current_location,
+ routing_path,
+ return_to="calculated-summary,grand-calculated-summary",
+ return_to_answer_id="distance-calculated-summary-1",
+ return_to_block_id="distance-calculated-summary-1,distance-grand-calculated-summary",
+ )
+
+ expected_previous_url = url_for(
+ "questionnaire.block",
+ return_to="grand-calculated-summary",
+ block_id="distance-calculated-summary-1",
+ return_to_block_id="distance-grand-calculated-summary",
+ _anchor="distance-calculated-summary-1",
+ )
+
+ assert expected_previous_url == next_location_url
+
+ @pytest.mark.usefixtures("app")
+ def test_return_to_grand_calculated_summary_from_calculated_summary(
+ self, grand_calculated_summary_progress_store, grand_calculated_summary_schema
+ ):
+ """
+ If going from GCS -> CS -> GCS this tests going from CS -> GCS having just come from the grand calculated summary
+ """
+ self.schema = grand_calculated_summary_schema
+ self.progress_store = grand_calculated_summary_progress_store
+
+ current_location = Location(
+ section_id="section-1", block_id="distance-calculated-summary-1"
+ )
+
+ routing_path = RoutingPath(
+ ["distance-calculated-summary-1"],
+ section_id="section-1",
+ )
+ next_location_url = self.router.get_next_location_url(
+ current_location,
+ routing_path,
+ return_to="grand-calculated-summary",
+ return_to_answer_id="distance-calculated-summary-1",
+ return_to_block_id="distance-grand-calculated-summary",
+ )
+
+ expected_previous_url = url_for(
+ "questionnaire.block",
+ return_to="grand-calculated-summary",
+ block_id="distance-grand-calculated-summary",
+ _anchor="distance-calculated-summary-1",
+ )
+
+ assert expected_previous_url == next_location_url
+
+ @pytest.mark.parametrize(
+ "return_to_block_id",
+ ("grand-calculated-summary-1", "grand-calculated-summary-2"),
+ )
+ @pytest.mark.usefixtures("app")
+ def test_return_to_grand_calculated_summary_from_incomplete_section(
+ self, return_to_block_id
+ ):
+ """
+ This tests that if you try to return to a grand calculated summary from an incomplete section
+ (or the same section but before the dependencies of the grand calculated summary are complete)
+ you are routed to the next block in the incomplete section rather than the grand calculated summary
+ """
+ self.schema = load_schema_from_name(
+ "test_grand_calculated_summary_multiple_sections"
+ )
+ # calculated summary 3 is not complete yet
+ self.progress_store = ProgressStore(
+ [
+ {
+ "section_id": "section-1",
+ "block_ids": [
+ "block-1",
+ "block-2",
+ "calculated-summary-1",
+ "block-3",
+ "calculated-summary-2",
+ ],
+ "status": "IN_PROGRESS",
+ }
+ ]
+ )
+
+ current_location = Location(section_id="section-1", block_id="block-2")
+ routing_path = RoutingPath(
+ [
+ "block-1",
+ "block-2",
+ "calculated-summary-1",
+ "block-3",
+ "calculated-summary-2",
+ "calculated-summary-3",
+ "grand-calculated-summary-1",
+ ],
+ section_id="section-1",
+ )
+ next_location_url = self.router.get_next_location_url(
+ current_location,
+ routing_path,
+ return_to="grand-calculated-summary",
+ return_to_answer_id="calculated-summary-1",
+ return_to_block_id=return_to_block_id,
+ )
+
+ # because calculated summary 3 isn't done, should go there before jumping to the grand calculated summary
+ # test from grand-calculated-summary-1 which is in the same section, and grand-calculated-summary-2 which is in another
+ expected_next_url = url_for(
+ "questionnaire.block",
+ return_to="grand-calculated-summary",
+ return_to_block_id=return_to_block_id,
+ block_id="calculated-summary-3",
+ )
+
+ assert expected_next_url == next_location_url
+
+ @pytest.mark.usefixtures("app")
+ def test_return_to_calculated_summary_from_incomplete_section(
+ self, grand_calculated_summary_schema
+ ):
+ """
+ This tests that if you try to return to a calculated summary section from an incomplete section
+ you are routed to the next block in the incomplete section rather than the calculated summary
+ """
+ self.schema = grand_calculated_summary_schema
+ # second-number block not complete yet
+ self.progress_store = ProgressStore(
+ [
+ {
+ "section_id": "section-1",
+ "block_ids": [
+ "first-number-block",
+ "distance-calculated-summary-1",
+ ],
+ "status": CompletionStatus.IN_PROGRESS,
+ }
+ ]
+ )
+
+ current_location = Location(
+ section_id="section-1", block_id="first-number-block"
+ )
+ routing_path = RoutingPath(
+ [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ "number-calculated-summary-1",
+ ],
+ section_id="section-1",
+ )
+ # the test is being done as part of a two-step return to but its identical functionally
+ next_location_url = self.router.get_next_location_url(
+ current_location,
+ routing_path,
+ return_to="calculated-summary,grand-calculated-summary",
+ return_to_answer_id="first-number-block",
+ return_to_block_id="distance-calculated-summary-1,distance-grand-calculated-summary",
+ )
+
+ # should take you to the second-number-block before going back to the calculated summary
+ expected_next_url = url_for(
+ "questionnaire.block",
+ return_to="calculated-summary,grand-calculated-summary",
+ return_to_block_id="distance-calculated-summary-1,distance-grand-calculated-summary",
+ block_id="second-number-block",
+ )
+
+ assert expected_next_url == next_location_url
+
class TestRouterNextLocationLinearFlow(RouterTestCase):
@pytest.mark.usefixtures("app")
@@ -826,6 +1065,173 @@ def test_return_to_calculated_summary(self):
assert expected_location_url == previous_location_url
+ @pytest.mark.usefixtures("app")
+ def test_return_to_grand_calculated_summary_from_answer_incomplete_section(
+ self, grand_calculated_summary_schema
+ ):
+ """
+ This tests that if you are on a calculated summary, and your return_to_block_id is another calculated summary that you cannot reach yet
+ if you click previous, then you are taken to the previous block in the section
+ (rather than the first incomplete block of the section which is what next location would return)
+ """
+ self.schema = grand_calculated_summary_schema
+ # trying to go to number-calculated-summary-1 but distance-calculated-summary-1 which comes before is not complete yet
+ self.progress_store = ProgressStore(
+ [
+ {
+ "section_id": "section-1",
+ "block_ids": [
+ "first-number-block",
+ "second-number-block",
+ "number-calculated-summary-1",
+ ],
+ "status": CompletionStatus.IN_PROGRESS,
+ }
+ ]
+ )
+
+ current_location = Location(
+ section_id="section-1", block_id="second-number-block"
+ )
+
+ routing_path = RoutingPath(
+ [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ "number-calculated-summary-1",
+ ],
+ section_id="section-1",
+ )
+ previous_location_url = self.router.get_previous_location_url(
+ current_location,
+ routing_path,
+ return_to="calculated-summary,grand-calculated-summary",
+ return_to_answer_id="second-number-block",
+ return_to_block_id="number-calculated-summary-1,number-grand-calculated-summary",
+ )
+ # return to can't go to the distance calculated summary, so go to previous block with return params preserved
+ expected_previous_url = url_for(
+ "questionnaire.block",
+ return_to="calculated-summary,grand-calculated-summary",
+ return_to_block_id="number-calculated-summary-1,number-grand-calculated-summary",
+ block_id="first-number-block",
+ _anchor="second-number-block",
+ )
+
+ assert expected_previous_url == previous_location_url
+
+ @pytest.mark.usefixtures("app")
+ def test_return_to_grand_calculated_summary_from_calculated_summary_incomplete_section(
+ self, grand_calculated_summary_schema
+ ):
+ """
+ This tests that if you are on a calculated summary, and your return_to_block_id is a grand calculated summary
+ if you click previous, then you are taken to the previous block in the section
+ (rather than the first incomplete block of the section which is what next location would return)
+ """
+ self.schema = grand_calculated_summary_schema
+ # number calculated summary is not complete, so the section is not complete
+ self.progress_store = ProgressStore(
+ [
+ {
+ "section_id": "section-1",
+ "block_ids": [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ ],
+ "status": CompletionStatus.IN_PROGRESS,
+ }
+ ]
+ )
+
+ current_location = Location(
+ section_id="section-1", block_id="distance-calculated-summary-1"
+ )
+
+ routing_path = RoutingPath(
+ [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ "number-calculated-summary-1",
+ ],
+ section_id="section-1",
+ )
+ previous_location_url = self.router.get_previous_location_url(
+ current_location,
+ routing_path,
+ return_to="grand-calculated-summary",
+ return_to_answer_id="distance-calculated-summary-1",
+ return_to_block_id="distance-grand-calculated-summary",
+ )
+ # return to can't go to the grand calculated summary, so routing is just to the previous block in the section with return params preserved
+ expected_previous_url = url_for(
+ "questionnaire.block",
+ return_to="grand-calculated-summary",
+ return_to_block_id="distance-grand-calculated-summary",
+ block_id="second-number-block",
+ _anchor="distance-calculated-summary-1",
+ )
+
+ assert expected_previous_url == previous_location_url
+
+ @pytest.mark.parametrize(
+ "return_to, current_block, return_to_block_id, expected_url",
+ [
+ (
+ "grand-calculated-summary",
+ "distance-calculated-summary-1",
+ "invalid-block",
+ "/questionnaire/second-number-block/?return_to=grand-calculated-summary&return_to_block_id=invalid-block#distance-calculated-summary-1",
+ ),
+ (
+ "calculated-summary,invalid",
+ "second-number-block",
+ "invalid-1,invalid-2",
+ "/questionnaire/first-number-block/?return_to=calculated-summary,invalid&return_to_block_id=invalid-1,invalid-2#distance-calculated-summary-1",
+ ),
+ (
+ "invalid",
+ "distance-calculated-summary-1",
+ "first-number-block",
+ "/questionnaire/second-number-block/?return_to=invalid&return_to_block_id=first-number-block#distance-calculated-summary-1",
+ ),
+ ],
+ )
+ @pytest.mark.usefixtures("app")
+ def test_return_to_grand_calculated_summary_invalid_url(
+ self,
+ return_to,
+ current_block,
+ return_to_block_id,
+ expected_url,
+ grand_calculated_summary_schema,
+ ):
+ self.schema = grand_calculated_summary_schema
+
+ current_location = Location(section_id="section-1", block_id=current_block)
+
+ routing_path = RoutingPath(
+ [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ "number-calculated-summary-1",
+ ],
+ section_id="section-1",
+ )
+ previous_location_url = self.router.get_previous_location_url(
+ current_location,
+ routing_path,
+ return_to=return_to,
+ return_to_answer_id="distance-calculated-summary-1",
+ return_to_block_id=return_to_block_id,
+ )
+
+ assert expected_url == previous_location_url
+
@pytest.mark.usefixtures("app")
def test_return_to_section_summary_section_is_complete(self):
self.schema = load_schema_from_name("test_section_summary")
diff --git a/tests/app/views/contexts/__init__.py b/tests/app/views/contexts/__init__.py
index e929f99c7a..38e5cfe793 100644
--- a/tests/app/views/contexts/__init__.py
+++ b/tests/app/views/contexts/__init__.py
@@ -1,4 +1,4 @@
-def assert_summary_context(context):
+def assert_summary_context(context, summary_item_type="question"):
summary_context = context["summary"]
for key_value in ("sections", "answers_are_editable", "summary_type"):
assert (
@@ -10,10 +10,10 @@ def assert_summary_context(context):
assert "id" in group
assert "blocks" in group
for block in group["blocks"]:
- assert "question" in block
- assert "title" in block["question"]
- assert "answers" in block["question"]
- for answer in block["question"]["answers"]:
+ assert summary_item_type in block
+ assert "title" in block[summary_item_type]
+ assert "answers" in block[summary_item_type]
+ for answer in block[summary_item_type]["answers"]:
assert "id" in answer
assert "value" in answer
assert "type" in answer
diff --git a/tests/app/views/contexts/conftest.py b/tests/app/views/contexts/conftest.py
index 35bfffb073..d10dd27c78 100644
--- a/tests/app/views/contexts/conftest.py
+++ b/tests/app/views/contexts/conftest.py
@@ -329,6 +329,11 @@ def test_calculated_summary_schema():
return load_schema_from_name("test_calculated_summary")
+@pytest.fixture
+def test_grand_calculated_summary_schema():
+ return load_schema_from_name("test_grand_calculated_summary")
+
+
@pytest.fixture
def test_calculated_summary_answers():
answers = [
@@ -349,6 +354,21 @@ def test_calculated_summary_answers():
return AnswerStore(answers)
+@pytest.fixture
+def test_grand_calculated_summary_answers():
+ answers = [
+ {"value": 10, "answer_id": "q1-a1"},
+ {"value": 1, "answer_id": "q1-a2"},
+ {"value": 20, "answer_id": "q2-a1"},
+ {"value": 2, "answer_id": "q2-a2"},
+ {"value": 30, "answer_id": "q3-a1"},
+ {"value": 3, "answer_id": "q3-a2"},
+ {"value": 40, "answer_id": "q4-a1"},
+ {"value": 4, "answer_id": "q4-a2"},
+ ]
+ return AnswerStore(answers)
+
+
@pytest.fixture
def test_calculated_summary_answers_skipped_fourth():
answers = [
diff --git a/tests/app/views/contexts/test_calculated_summary_context.py b/tests/app/views/contexts/test_calculated_summary_context.py
index 9811e82762..8e9e4f4dab 100644
--- a/tests/app/views/contexts/test_calculated_summary_context.py
+++ b/tests/app/views/contexts/test_calculated_summary_context.py
@@ -122,7 +122,7 @@ def test_build_view_context_for_currency_calculated_summary(
current_location=Location(section_id="default-section", block_id=block_id),
)
- context = calculated_summary_context.build_view_context_for_calculated_summary()
+ context = calculated_summary_context.build_view_context()
assert "summary" in context
assert_summary_context(context)
@@ -145,3 +145,90 @@ def test_build_view_context_for_currency_calculated_summary(
assert "return_to=calculated-summary" in answer_change_link
assert f"return_to_answer_id={return_to_answer_id}" in answer_change_link
assert f"return_to_block_id={block_id}" in answer_change_link
+
+
+@pytest.mark.usefixtures("app")
+@pytest.mark.parametrize(
+ "block_id, return_to_answer_id, return_to, return_to_block_id",
+ (
+ (
+ "distance-calculated-summary-1",
+ "q1-a1",
+ "grand-calculated-summary",
+ "distance-grand-calculated-summary",
+ ),
+ (
+ "number-calculated-summary-1",
+ "q1-a2",
+ "grand-calculated-summary",
+ "number-grand-calculated-summary",
+ ),
+ (
+ "distance-calculated-summary-2",
+ "q3-a1",
+ "grand-calculated-summary",
+ "distance-grand-calculated-summary",
+ ),
+ (
+ "number-calculated-summary-2",
+ "q3-a2",
+ "grand-calculated-summary",
+ "number-grand-calculated-summary",
+ ),
+ ),
+)
+def test_build_view_context_for_return_to_calculated_summary(
+ test_grand_calculated_summary_schema,
+ test_grand_calculated_summary_answers,
+ list_store,
+ progress_store,
+ mocker,
+ block_id,
+ return_to_answer_id,
+ return_to,
+ return_to_block_id,
+):
+ """
+ Tests the change answer links for a calculated summary that has been reached by a change link on a grand calculated summary
+ """
+ mocker.patch(
+ "app.jinja_filters.flask_babel.get_locale",
+ mocker.MagicMock(return_value="en_GB"),
+ )
+
+ block_ids = [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ "number-calculated-summary-1",
+ "third-number-block",
+ "fourth-number-block",
+ "distance-calculated-summary-2",
+ "number-calculated-summary-2",
+ ]
+
+ calculated_summary_context = CalculatedSummaryContext(
+ language="en",
+ schema=test_grand_calculated_summary_schema,
+ answer_store=test_grand_calculated_summary_answers,
+ list_store=list_store,
+ progress_store=progress_store,
+ metadata=None,
+ response_metadata={},
+ routing_path=RoutingPath(section_id="default-section", block_ids=block_ids),
+ current_location=Location(section_id="default-section", block_id=block_id),
+ return_to=return_to,
+ return_to_block_id=return_to_block_id,
+ )
+
+ context = calculated_summary_context.build_view_context()
+ assert "summary" in context
+ assert_summary_context(context)
+ context_summary = context["summary"]
+
+ answer_change_link = context_summary["sections"][0]["groups"][0]["blocks"][0][
+ "question"
+ ]["answers"][0]["link"]
+ assert f"return_to=calculated-summary,{return_to}" in answer_change_link
+ assert f"return_to_answer_id={return_to_answer_id}" in answer_change_link
+ assert f"return_to_block_id={block_id},{return_to_block_id}" in answer_change_link
diff --git a/tests/app/views/contexts/test_grand_calculated_summary_context.py b/tests/app/views/contexts/test_grand_calculated_summary_context.py
new file mode 100644
index 0000000000..3c53d30390
--- /dev/null
+++ b/tests/app/views/contexts/test_grand_calculated_summary_context.py
@@ -0,0 +1,111 @@
+import pytest
+
+from app.data_models.progress_store import CompletionStatus, ProgressStore
+from app.questionnaire import Location
+from app.questionnaire.routing_path import RoutingPath
+from app.views.contexts.grand_calculated_summary_context import (
+ GrandCalculatedSummaryContext,
+)
+from tests.app.views.contexts import assert_summary_context
+
+
+@pytest.mark.usefixtures("app")
+@pytest.mark.parametrize(
+ "block_id, title, value, return_to_answer_id",
+ (
+ (
+ "distance-grand-calculated-summary",
+ "We calculate the grand total weekly distance travelled to be 100 mi. Is this correct?",
+ "100 mi",
+ "distance-calculated-summary-1",
+ ),
+ (
+ "number-grand-calculated-summary",
+ "We calculate the grand total journeys per week to be 10. Is this correct?",
+ "10",
+ "number-calculated-summary-1",
+ ),
+ ),
+)
+def test_build_view_context_for_grand_calculated_summary(
+ block_id,
+ title,
+ value,
+ test_grand_calculated_summary_schema,
+ test_grand_calculated_summary_answers,
+ list_store,
+ mocker,
+ return_to_answer_id,
+):
+ mocker.patch(
+ "app.jinja_filters.flask_babel.get_locale",
+ mocker.MagicMock(return_value="en_GB"),
+ )
+
+ block_ids = [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ "number-calculated-summary-1",
+ "third-number-block",
+ "fourth-number-block",
+ "distance-calculated-summary-2",
+ "number-calculated-summary-2",
+ ]
+
+ grand_calculated_summary_context = GrandCalculatedSummaryContext(
+ language="en",
+ schema=test_grand_calculated_summary_schema,
+ answer_store=test_grand_calculated_summary_answers,
+ list_store=list_store,
+ progress_store=ProgressStore(
+ in_progress_sections=[
+ {
+ "section_id": "section-1",
+ "status": CompletionStatus.COMPLETED,
+ "block_ids": [
+ "first-number-block",
+ "second-number-block",
+ "distance-calculated-summary-1",
+ "number-calculated-summary-1",
+ ],
+ },
+ {
+ "section_id": "section-2",
+ "status": CompletionStatus.COMPLETED,
+ "block_ids": [
+ "third-number-block",
+ "fourth-number-block",
+ "distance-calculated-summary-2",
+ "number-calculated-summary-2",
+ ],
+ },
+ ]
+ ),
+ metadata=None,
+ response_metadata={},
+ routing_path=RoutingPath(section_id="default-section", block_ids=block_ids),
+ current_location=Location(section_id="default-section", block_id=block_id),
+ return_to=None,
+ return_to_block_id=None,
+ )
+
+ context = grand_calculated_summary_context.build_view_context()
+
+ assert "summary" in context
+ assert_summary_context(context, "calculated_summary")
+ assert len(context["summary"]) == 6
+ context_summary = context["summary"]
+ assert context_summary.get("title") == title
+
+ assert "calculated_question" in context_summary
+ assert context_summary["calculated_question"]["answers"][0]["value"] == value
+
+ calculated_summary_change_link = context_summary["sections"][0]["groups"][0][
+ "blocks"
+ ][0]["calculated_summary"]["answers"][0]["link"]
+ assert "return_to=grand-calculated-summary" in calculated_summary_change_link
+ assert (
+ f"return_to_answer_id={return_to_answer_id}" in calculated_summary_change_link
+ )
+ assert f"return_to_block_id={block_id}" in calculated_summary_change_link
diff --git a/tests/functional/base_pages/grand-calculated-summary.page.js b/tests/functional/base_pages/grand-calculated-summary.page.js
new file mode 100644
index 0000000000..4f1c9e3312
--- /dev/null
+++ b/tests/functional/base_pages/grand-calculated-summary.page.js
@@ -0,0 +1,17 @@
+import BasePage from "./base.page";
+
+class GrandCalculatedSummaryPage extends BasePage {
+ grandCalculatedSummaryTitle() {
+ return '[data-qa="grand-calculated-summary-title"]';
+ }
+
+ grandCalculatedSummaryQuestion() {
+ return "[data-qa=grand-calculated-summary-question]";
+ }
+
+ grandCalculatedSummaryAnswer() {
+ return "[data-qa=grand-calculated-summary-answer]";
+ }
+}
+
+export default GrandCalculatedSummaryPage;
diff --git a/tests/functional/generate_pages.py b/tests/functional/generate_pages.py
index b20998f7df..0b5910ae9b 100755
--- a/tests/functional/generate_pages.py
+++ b/tests/functional/generate_pages.py
@@ -848,6 +848,10 @@ def process_block(
base_page = "CalculatedSummaryPage"
base_page_file = "calculated-summary.page"
+ if block["type"] == "GrandCalculatedSummary":
+ base_page = "GrandCalculatedSummaryPage"
+ base_page_file = "grand-calculated-summary.page"
+
if block["type"] == "Introduction":
base_page = "IntroductionPageBase"
base_page_file = "introduction.page"
@@ -893,6 +897,21 @@ def process_block(
process_calculated_summary(calculated_summary_answer_ids, page_spec)
+ elif block["type"] == "GrandCalculatedSummary":
+ values = _get_dictionaries_with_key(
+ "source", block["calculation"]["operation"]
+ )
+
+ calculated_summary_ids = [
+ value["identifier"]
+ for value in values
+ if value["source"] == "calculated_summary"
+ ]
+
+ # each calculated summary in a grand calculated summary is constructed such that it will have a single "answer" linking back to it
+ # so the processing for calculated summaries can be directly reused.
+ process_calculated_summary(calculated_summary_ids, page_spec)
+
elif block["type"] == "Interstitial":
has_definition = False
if "content_variants" in block:
diff --git a/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_cross_section_dependencies.spec.js b/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_cross_section_dependencies.spec.js
new file mode 100644
index 0000000000..13dd580147
--- /dev/null
+++ b/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_cross_section_dependencies.spec.js
@@ -0,0 +1,120 @@
+import SkipFirstBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/skip-first-block.page";
+import SecondNumberBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/second-number-block.page";
+import HubPage from "../../../base_pages/hub.page";
+import CurrencySection1Page from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/currency-section-1.page";
+import QuestionsSectionSummaryPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/questions-section-summary.page";
+import ThirdNumberBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/third-number-block.page";
+import SkipCalculatedSummaryPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/skip-calculated-summary.page";
+import CalculatedSummarySectionSummaryPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/calculated-summary-section-summary.page";
+import CurrencyQuestion3Page from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/currency-question-3.page";
+import CurrencyAllPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/currency-all.page";
+import FirstNumberBlockPartAPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/first-number-block-part-a.page";
+import FourthNumberBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/fourth-number-block.page";
+
+describe("Feature: Grand Calculated Summary", () => {
+ describe("Given I have a Grand Calculated Summary", () => {
+ before("Getting to the second calculated summary", async () => {
+ await browser.openQuestionnaire("test_grand_calculated_summary_cross_section_dependencies.json");
+ await $(HubPage.submit()).click();
+ await $(SkipFirstBlockPage.no()).click();
+ await $(SkipFirstBlockPage.submit()).click();
+ await $(FirstNumberBlockPartAPage.firstNumberA()).setValue(300);
+ await $(FirstNumberBlockPartAPage.submit()).click();
+ await $(SecondNumberBlockPage.secondNumberA()).setValue(10);
+ await $(SecondNumberBlockPage.secondNumberB()).setValue(5);
+ await $(SecondNumberBlockPage.secondNumberC()).setValue(15);
+ await $(SecondNumberBlockPage.submit()).click();
+ await $(CurrencySection1Page.submit()).click();
+ await $(QuestionsSectionSummaryPage.submit()).click();
+ // section 2
+ await $(HubPage.submit()).click();
+ await $(ThirdNumberBlockPage.thirdNumberPartA()).setValue(70);
+ await $(ThirdNumberBlockPage.submit()).click();
+ });
+ it("Given I don't skip the second calculated summary, it is included in the grand calculated summary", async () => {
+ await $(SkipCalculatedSummaryPage.no()).click();
+ await $(SkipCalculatedSummaryPage.submit()).click();
+ await $(CurrencyQuestion3Page.submit()).click();
+ await $(CalculatedSummarySectionSummaryPage.submit()).click();
+ await $(HubPage.submit()).click();
+ await expect(await $(CurrencyAllPage.currencySection1()).getText()).to.contain("£330.00");
+ await expect(await $(CurrencyAllPage.currencyQuestion3()).getText()).to.contain("£70.00");
+ await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "The grand calculated summary is calculated to be £400.00. Is this correct?"
+ );
+ await $(CurrencyAllPage.submit()).click();
+ });
+ it("Given I go back and skip the second calculated summary, it is not included in the grand calculated summary", async () => {
+ await $(HubPage.summaryRowLink("calculated-summary-section")).click();
+ await $(CalculatedSummarySectionSummaryPage.skipAnswer2Edit()).click();
+ await $(SkipCalculatedSummaryPage.yes()).click();
+ await $(SkipCalculatedSummaryPage.submit()).click();
+ await $(CalculatedSummarySectionSummaryPage.submit()).click();
+ // Currently the grand calculated summary remains 'Completed' because none of the answers have changed
+ await expect(await $(HubPage.summaryRowState("grand-calculated-summary-section")).getText()).to.equal("Completed");
+ await $(HubPage.summaryRowLink("grand-calculated-summary-section")).click();
+ await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "The grand calculated summary is calculated to be £330.00. Is this correct?"
+ );
+ await expect(await $(CurrencyAllPage.currencyQuestion3()).isExisting()).to.be.false;
+ });
+ it("Given I confirm the grand calculated summary, then edit an answer for question 3, the grand calculated summary updates to be incomplete, because this is a dependency", async () => {
+ await $(CurrencyAllPage.submit()).click();
+ await $(HubPage.summaryRowLink("calculated-summary-section")).click();
+ await $(CalculatedSummarySectionSummaryPage.thirdNumberAnswerPartAEdit()).click();
+ await $(ThirdNumberBlockPage.thirdNumberPartA()).setValue(130);
+ await $(ThirdNumberBlockPage.submit()).click();
+ await $(CalculatedSummarySectionSummaryPage.submit()).click();
+ // Although the calculated summary is not on the path, the answer is still a grand calculated summary dependency, so it updates progress
+ await expect(await $(HubPage.summaryRowState("grand-calculated-summary-section")).getText()).to.equal("Partially completed");
+ await $(HubPage.summaryRowLink("grand-calculated-summary-section")).click();
+ await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "The grand calculated summary is calculated to be £330.00. Is this correct?"
+ );
+ await expect(await $(CurrencyAllPage.currencyQuestion3()).isExisting()).to.be.false;
+ await $(CurrencyAllPage.submit()).click();
+ });
+ it("Given I change my response to include the calculated summary, the grand calculated summary updates to re-include it", async () => {
+ await $(HubPage.summaryRowLink("calculated-summary-section")).click();
+ await $(CalculatedSummarySectionSummaryPage.skipAnswer2Edit()).click();
+ await $(SkipCalculatedSummaryPage.no()).click();
+ await $(SkipCalculatedSummaryPage.submit()).click();
+ await $(CurrencyQuestion3Page.submit()).click();
+ await $(CalculatedSummarySectionSummaryPage.submit()).click();
+ // Currently, the grand calculated summary does not return to in-progress, because none of the answers it depends on have changed
+ await expect(await $(HubPage.summaryRowState("grand-calculated-summary-section")).getText()).to.equal("Completed");
+ await $(HubPage.summaryRowLink("grand-calculated-summary-section")).click();
+ await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "The grand calculated summary is calculated to be £460.00. Is this correct?"
+ );
+ });
+ it("Given I provide an answer to question 3b from the grand calculated summary, this opens up an additional question, and when I press continue I am taken to this question first, then the calculated summary, and then the grand calculated summary", async () => {
+ await $(CurrencyAllPage.currencyQuestion3Edit()).click();
+ await $(CurrencyQuestion3Page.thirdNumberAnswerPartBEdit()).click();
+ await $(ThirdNumberBlockPage.thirdNumberPartB()).setValue(10);
+ await $(ThirdNumberBlockPage.submit()).click();
+ await expect(await browser.getUrl()).to.contain(FourthNumberBlockPage.pageName);
+ await $(FourthNumberBlockPage.fourthNumber()).setValue(1);
+ await $(FourthNumberBlockPage.submit()).click();
+ await expect(await browser.getUrl()).to.contain(CurrencyQuestion3Page.pageName);
+ await $(CurrencyQuestion3Page.submit()).click();
+ await expect(await browser.getUrl()).to.contain(CurrencyAllPage.pageName);
+ await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "The grand calculated summary is calculated to be £471.00. Is this correct?"
+ );
+ await $(CurrencyAllPage.submit()).click();
+ });
+ it("Given I go back to section one and skip the first block, it is not included in the first calculated summary and consequently not included in the grand calculated summary", async () => {
+ await $(HubPage.summaryRowLink("questions-section")).click();
+ await $(QuestionsSectionSummaryPage.skipAnswer1Edit()).click();
+ await $(SkipFirstBlockPage.yes()).click();
+ await $(SkipFirstBlockPage.submit()).click();
+ await $(QuestionsSectionSummaryPage.submit()).click();
+ await $(HubPage.summaryRowLink("grand-calculated-summary-section")).click();
+ await expect(await $(CurrencyAllPage.currencySection1()).getText()).to.contain("£30.00");
+ await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "The grand calculated summary is calculated to be £171.00. Is this correct?"
+ );
+ });
+ });
+});
diff --git a/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_multiple_sections.spec.js b/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_multiple_sections.spec.js
new file mode 100644
index 0000000000..d9a3ed2a9c
--- /dev/null
+++ b/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_multiple_sections.spec.js
@@ -0,0 +1,154 @@
+import HubPage from "../../../base_pages/hub.page";
+import Block1Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/block-1.page";
+import Block2Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/block-2.page";
+import CalculatedSummary1Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/calculated-summary-1.page";
+import Block3Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/block-3.page";
+import Block4Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/block-4.page";
+import CalculatedSummary2Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/calculated-summary-2.page";
+import CalculatedSummary3Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/calculated-summary-3.page";
+import CalculatedSummary4Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/calculated-summary-4.page";
+import GrandCalculatedSummary1Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/grand-calculated-summary-1.page";
+import GrandCalculatedSummary2Page from "../../../generated_pages/grand_calculated_summary_multiple_sections/grand-calculated-summary-2.page";
+import Section1SummaryPage from "../../../generated_pages/grand_calculated_summary_multiple_sections/section-1-summary.page";
+
+describe("Feature: Grand Calculated Summary", () => {
+ describe("Given I have a Grand Calculated Summary across multiple sections", () => {
+ before("Reaching the grand calculated summary section", async () => {
+ await browser.openQuestionnaire("test_grand_calculated_summary_multiple_sections.json");
+ await $(HubPage.submit()).click();
+
+ // complete 2 questions in section 1
+ await $(Block1Page.q1A1()).setValue(10);
+ await $(Block1Page.q1A2()).setValue(20);
+ await $(Block1Page.submit()).click();
+ await $(Block2Page.q2A1()).setValue(30);
+ await $(Block2Page.q2A2()).setValue(40);
+ await $(Block2Page.submit()).click();
+ await $(CalculatedSummary1Page.submit()).click();
+
+ // and the one for section 2
+ await $(Block3Page.q3A1()).setValue(100);
+ await $(Block3Page.q3A2()).setValue(200);
+ await $(Block3Page.submit()).click();
+ await $(CalculatedSummary2Page.submit()).click();
+ await $(CalculatedSummary3Page.submit()).click();
+ await $(GrandCalculatedSummary1Page.submit()).click();
+ await $(Section1SummaryPage.submit()).click();
+ await $(HubPage.submit()).click();
+ await $(Block4Page.q4A1()).setValue(5);
+ await $(Block4Page.q4A2()).setValue(10);
+ await $(Block4Page.submit()).click();
+ await $(CalculatedSummary4Page.submit()).click();
+ await $(HubPage.submit()).click();
+ });
+
+ it("Given I click on the change link for a calculated summary then press continue, I am taken back to the grand calculated summary", async () => {
+ await expect(await $(GrandCalculatedSummary2Page.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "Grand Calculated Summary for section 1 and 2 is calculated to be £415.00. Is this correct?"
+ );
+ await $(GrandCalculatedSummary2Page.calculatedSummary1Edit()).click();
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary1Page.pageName);
+
+ await $(CalculatedSummary1Page.submit()).click();
+ await expect(await browser.getUrl()).to.contain(GrandCalculatedSummary2Page.pageName);
+ });
+
+ it("Given I go back to the calculated summary and then to a question and edit the answer. I am first taken back to the each calculated summary that uses the answer, the grand calculated summary in section 1, and then the updated grand calculated summary in section 3.", async () => {
+ await $(GrandCalculatedSummary2Page.calculatedSummary4Edit()).click();
+ await expect(await $(CalculatedSummary4Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Calculated Summary for Question 4 is calculated to be £15.00. Is this correct?"
+ );
+ await $(CalculatedSummary4Page.q4A1Edit()).click();
+ await expect(await browser.getUrl()).to.contain(Block4Page.pageName);
+
+ await $(Block4Page.q4A1()).setValue(50);
+ await $(Block4Page.submit()).click();
+
+ // first taken back to the calculated summary which has updated
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary4Page.pageName);
+ await expect(await $(CalculatedSummary4Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Calculated Summary for Question 4 is calculated to be £60.00. Is this correct?"
+ );
+ await $(CalculatedSummary4Page.submit()).click();
+
+ // then taken back to the grand calculated summary which has also been updated correctly
+ await expect(await browser.getUrl()).to.contain(GrandCalculatedSummary2Page.pageName);
+ await expect(await $(GrandCalculatedSummary2Page.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "Grand Calculated Summary for section 1 and 2 is calculated to be £460.00. Is this correct?"
+ );
+ });
+
+ it("Given I go back to another calculated summary and edit multiple answers, I am still correctly routed back to the grand calculated summary", async () => {
+ await $(GrandCalculatedSummary2Page.calculatedSummary1Edit()).click();
+ await expect(await $(CalculatedSummary1Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Calculated Summary for Question 1 is calculated to be £100.00. Is this correct?"
+ );
+
+ // change first answer
+ await $(CalculatedSummary1Page.q1A1Edit()).click();
+ await expect(await browser.getUrl()).to.contain(Block1Page.pageName);
+ await $(Block1Page.q1A1()).setValue(100);
+ await $(Block1Page.submit()).click();
+
+ // go to each calculated summary that uses the answer in turn, then each grand calculated summary up to the one we were editing
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary1Page.pageName);
+ await expect(await $(CalculatedSummary1Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Calculated Summary for Question 1 is calculated to be £190.00. Is this correct?"
+ );
+
+ // change another answer
+ await $(CalculatedSummary1Page.q2A2Edit()).click();
+ await expect(await browser.getUrl()).to.contain(Block2Page.pageName);
+ await $(Block2Page.q2A2()).setValue(400);
+ await $(Block2Page.submit()).click();
+
+ // back at updated calculated summary
+ await expect(await $(CalculatedSummary1Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Calculated Summary for Question 1 is calculated to be £550.00. Is this correct?"
+ );
+
+ // Go to each calculated/grand calculated summary including this answer and reconfirm before being taken back to grand calculated summary
+ await $(CalculatedSummary1Page.submit()).click();
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary3Page.pageName);
+ await $(CalculatedSummary3Page.submit()).click();
+ await expect(await browser.getUrl()).to.contain(GrandCalculatedSummary1Page.pageName);
+ await $(GrandCalculatedSummary1Page.submit()).click();
+ await expect(await browser.getUrl()).to.contain(GrandCalculatedSummary2Page.pageName);
+ await expect(await $(GrandCalculatedSummary2Page.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "Grand Calculated Summary for section 1 and 2 is calculated to be £910.00. Is this correct?"
+ );
+ });
+
+ it("Given I edit an answer included in a grand calculated summary, both the calculated and grand calculated summary sections should return to partially completed.", async () => {
+ await $(GrandCalculatedSummary2Page.submit()).click();
+ await expect(await $(HubPage.summaryRowState("section-3")).getText()).to.equal("Completed");
+
+ // Now edit an answer from section 2 and go back to the hub
+ await $(HubPage.summaryRowLink("section-3")).click();
+ await $(GrandCalculatedSummary2Page.calculatedSummary4Edit()).click();
+ await $(CalculatedSummary4Page.q4A1Edit()).click();
+ await $(Block4Page.q4A1()).setValue(1);
+ await $(Block4Page.submit()).click();
+ await $(CalculatedSummary4Page.previous()).click();
+ await $(Block4Page.previous()).click();
+
+ // calculated summary section should be in progress
+ await expect(await $(HubPage.summaryRowState("section-2")).getText()).to.equal("Partially completed");
+ // TODO: grand calculated summary should not show, but this requires progress source, until this is implemented, it should at least show as in progress
+ await expect(await $(HubPage.summaryRowState("section-3")).getText()).to.equal("Partially completed");
+ });
+
+ it("Given I set both answers to block 4 to zero which removes the Grand Calculated Summary from the path, I am routed back to the Hub after the calculated summary", async () => {
+ await $(HubPage.summaryRowLink("section-3")).click();
+ await $(GrandCalculatedSummary2Page.calculatedSummary4Edit()).click();
+ await $(CalculatedSummary4Page.q4A1Edit()).click();
+ await $(Block4Page.q4A1()).setValue(0);
+ await $(Block4Page.q4A2()).setValue(0);
+ await $(Block4Page.submit()).click();
+ await $(CalculatedSummary4Page.submit()).click();
+ // should be back at Hub, and grand calculated summary section not present
+ await expect(await browser.getUrl()).to.contain(HubPage.pageName);
+ await expect(await $(HubPage.summaryRowLink("section-3")).isExisting()).to.be.false;
+ });
+ });
+});
diff --git a/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_overlapping_answers.spec.js b/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_overlapping_answers.spec.js
new file mode 100644
index 0000000000..c466bad957
--- /dev/null
+++ b/tests/functional/spec/features/grand_calculated_summary/grand_calculated_summary_overlapping_answers.spec.js
@@ -0,0 +1,130 @@
+import HubPage from "../../../base_pages/hub.page";
+import IntroductionBlockPage from "../../../generated_pages/grand_calculated_summary_overlapping_answers/introduction-block.page";
+import Block1Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/block-1.page";
+import Block2Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/block-2.page";
+import CalculatedSummary1Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/calculated-summary-1.page";
+import CalculatedSummary2Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/calculated-summary-2.page";
+import Block3Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/block-3.page";
+import CalculatedSummary4Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/calculated-summary-4.page";
+import GrandCalculatedSummaryShoppingPage from "../../../generated_pages/grand_calculated_summary_overlapping_answers/grand-calculated-summary-shopping.page";
+import Section1SummaryPage from "../../../generated_pages/grand_calculated_summary_overlapping_answers/section-1-summary.page";
+
+describe("Feature: Grand Calculated Summary", () => {
+ describe("Given I have a Grand Calculated Summary with overlapping answers", () => {
+ before("completing the survey", async () => {
+ await browser.openQuestionnaire("test_grand_calculated_summary_overlapping_answers.json");
+ await $(IntroductionBlockPage.submit()).click();
+
+ // grand calculated summary should not be enabled until section-1 complete
+ await expect(await $(HubPage.summaryRowLink("section-3")).isExisting()).to.be.false;
+
+ await $(HubPage.submit()).click();
+ await $(Block1Page.q1A1()).setValue(100);
+ await $(Block1Page.q1A2()).setValue(200);
+ await $(Block1Page.submit()).click();
+ await $(Block2Page.q2A1()).setValue(10);
+ await $(Block2Page.q2A2()).setValue(20);
+ await $(Block2Page.submit()).click();
+ await $(CalculatedSummary1Page.submit()).click();
+ await $(CalculatedSummary2Page.submit()).click();
+ await $(Block3Page.yesExtraBreadAndCheese()).click();
+ await $(Block3Page.submit()).click();
+ await $(CalculatedSummary4Page.submit()).click();
+ await $(Section1SummaryPage.submit()).click();
+ await $(HubPage.submit()).click();
+ await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "Grand Calculated Summary of purchases this week comes to £360.00. Is this correct?"
+ );
+ await $(GrandCalculatedSummaryShoppingPage.submit()).click();
+ });
+
+ it("Given I edit an answer that is only used in a single calculated summary, I am routed back to the calculated summary and then the grand calculated summary", async () => {
+ await $(HubPage.summaryRowLink("section-3")).click();
+ await $(GrandCalculatedSummaryShoppingPage.calculatedSummary2Edit()).click();
+ await $(CalculatedSummary2Page.q1A2Edit()).click();
+ await $(Block1Page.q1A2()).setValue(300);
+ await $(Block1Page.submit()).click();
+
+ // taken back to calculated summary
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary2Page.pageName);
+ await $(CalculatedSummary2Page.submit()).click();
+
+ // then grand calculated summary
+ await expect(await browser.getUrl()).to.contain(GrandCalculatedSummaryShoppingPage.pageName);
+ await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "Grand Calculated Summary of purchases this week comes to £460.00. Is this correct?"
+ );
+ });
+
+ it("Given I edit an answer that is used in two calculated summaries, if I edit it from the first calculated summary change link, I taken through each block between the question and the second calculated summary before returning to the grand calculated summary", async () => {
+ await $(GrandCalculatedSummaryShoppingPage.calculatedSummary2Edit()).click();
+ await $(CalculatedSummary2Page.q2A2Edit()).click();
+ await $(Block2Page.q2A2()).setValue(400);
+ await $(Block2Page.submit()).click();
+
+ // taken back to the FIRST calculated summary which uses it
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary2Page.pageName);
+ await expect(await $(CalculatedSummary2Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Total of eggs and cheese is calculated to be £700.00. Is this correct?"
+ );
+ await $(CalculatedSummary2Page.submit()).click();
+
+ // taken back to the SECOND calculated summary which uses it
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary4Page.pageName);
+ await expect(await $(CalculatedSummary4Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Total extra items cost is calculated to be £410.00. Is this correct?"
+ );
+ await $(CalculatedSummary4Page.submit()).click();
+
+ // then grand calculated summary
+ await expect(await browser.getUrl()).to.contain(GrandCalculatedSummaryShoppingPage.pageName);
+ await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "Grand Calculated Summary of purchases this week comes to £1,220.00. Is this correct?"
+ );
+ });
+
+ it("Given I edit an answer that is used in two calculated summaries, if I edit it from the second calculated summary change link, I taken through each block between the question and the second calculated summary before returning to the grand calculated summary", async () => {
+ await $(GrandCalculatedSummaryShoppingPage.calculatedSummary4Edit()).click();
+ await $(CalculatedSummary4Page.q2A2Edit()).click();
+ await $(Block2Page.q2A2()).setValue(500);
+ await $(Block2Page.submit()).click();
+
+ // taken back to the FIRST calculated summary which uses it
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary2Page.pageName);
+ await expect(await $(CalculatedSummary2Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Total of eggs and cheese is calculated to be £800.00. Is this correct?"
+ );
+ await $(CalculatedSummary2Page.submit()).click();
+
+ // taken back to the SECOND calculated summary which uses it
+ await expect(await browser.getUrl()).to.contain(CalculatedSummary4Page.pageName);
+ await expect(await $(CalculatedSummary4Page.calculatedSummaryTitle()).getText()).to.contain(
+ "Total extra items cost is calculated to be £510.00. Is this correct?"
+ );
+ await $(CalculatedSummary4Page.submit()).click();
+
+ // then grand calculated summary
+ await expect(await browser.getUrl()).to.contain(GrandCalculatedSummaryShoppingPage.pageName);
+ await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).to.contain(
+ "Grand Calculated Summary of purchases this week comes to £1,420.00. Is this correct?"
+ );
+ await $(GrandCalculatedSummaryShoppingPage.submit()).click();
+ });
+
+ it("Given I change an answer and return to the Hub before all calculated summaries are confirmed, the grand calculated summary section becomes inaccessible", async () => {
+ await $(HubPage.summaryRowLink("section-3")).click();
+ await $(GrandCalculatedSummaryShoppingPage.calculatedSummary4Edit()).click();
+ await $(CalculatedSummary4Page.q2A2Edit()).click();
+ await $(Block2Page.q2A2()).setValue(100);
+ await $(Block2Page.submit()).click();
+
+ // confirm one of the calculated summaries but return to the hub instead of confirming the other
+ await $(CalculatedSummary2Page.submit()).click();
+ await browser.url(HubPage.url());
+
+ // calculated summary 4 is not confirmed so GCS doesn't show
+ await expect(await $(HubPage.summaryRowState("section-1")).getText()).to.equal("Partially completed");
+ await expect(await $(HubPage.summaryRowLink("section-3")).isExisting()).to.be.false;
+ });
+ });
+});
diff --git a/tests/integration/questionnaire/test_questionnaire_grand_calculated_summary.py b/tests/integration/questionnaire/test_questionnaire_grand_calculated_summary.py
new file mode 100644
index 0000000000..3750564cc3
--- /dev/null
+++ b/tests/integration/questionnaire/test_questionnaire_grand_calculated_summary.py
@@ -0,0 +1,163 @@
+from . import QuestionnaireTestCase
+
+
+class TestQuestionnaireGrandCalculatedSummary(QuestionnaireTestCase):
+ BASE_URL = "/questionnaire/"
+
+ def test_grand_calculated_summary(self):
+ self.launchSurvey("test_grand_calculated_summary")
+ # section-1 two types of unit questions
+ self.post({"q1-a1": 20, "q1-a2": 5})
+ self.post({"q2-a1": 100, "q2-a2": 3})
+ self.post()
+ self.post()
+ # section-2 two more of each question type
+ self.post({"q3-a1": 40, "q3-a2": 2})
+ self.post({"q4-a1": 10, "q4-a2": 3})
+ self.post()
+ self.post()
+ # check the two grand calculated summaries
+ self.assertInBody(
+ "We calculate the grand total weekly distance travelled to be 170 mi. Is this correct?"
+ )
+ self.post()
+ self.assertInBody(
+ "We calculate the grand total journeys per week to be 13. Is this correct?"
+ )
+
+ def test_grand_calculated_summary_multiple_sections(self):
+ self.launchSurvey("test_grand_calculated_summary_multiple_sections")
+ # section 1
+ self.post()
+ self.post({"q1-a1": 10, "q1-a2": 20})
+ self.post({"q2-a1": 30, "q2-a2": 40})
+ self.post()
+ self.post({"q3-a1": 50, "q3-a2": 60})
+ self.post()
+ # confirm calculated and grand calculated summary
+ self.assertInBody(
+ "Calculated summary for section 1 is calculated to be £210.00. Is this correct?"
+ )
+ self.post()
+ self.assertInBody(
+ "Grand Calculated Summary which should match the previous calculated summary is calculated to be £210.00. Is this correct?"
+ )
+ self.post()
+ self.post()
+ # section 2
+ self.post()
+ self.post({"q4-a1": 100, "q4-a2": 200})
+ self.post()
+ # grand calculated summary section with calculated summaries from multiple sections
+ self.post()
+ self.assertInBody(
+ "Grand Calculated Summary for section 1 and 2 is calculated to be £510.00. Is this correct?"
+ )
+
+ def _complete_upto_grand_calculated_summary_cross_section_dependencies(self):
+ """
+ Completes first two sections of the schema testing grand calculated summaries
+ depending on calculated summaries in other sections
+ """
+ # Complete the first section
+ self.post()
+ self.post({"skip-answer-1": "Yes"})
+ self.post({"second-number-answer-a": "30", "second-number-answer-b": "60"})
+ self.assertInBody(
+ "We calculate your total monthly expenditure on household bills to be £90.00. Is this correct?"
+ )
+ self.post()
+ self.post()
+ # Complete the second section
+ self.post()
+ self.post({"third-number-answer-part-a": "70"})
+
+ def test_grand_calculated_summary_cross_section_dependencies_with_skip(self):
+ self.launchSurvey("test_grand_calculated_summary_cross_section_dependencies")
+ self._complete_upto_grand_calculated_summary_cross_section_dependencies()
+
+ # skip the calculated summary and go straight to section summary
+ self.post({"skip-answer-2": "Yes"})
+ self.post()
+
+ # grand calculated summary which doesn't include skipped calculated summary
+ self.post()
+ self.assertInBody(
+ "The grand calculated summary is calculated to be £90.00. Is this correct?"
+ )
+
+ def test_grand_calculated_summary_cross_section_dependencies_no_skip(self):
+ self.launchSurvey("test_grand_calculated_summary_cross_section_dependencies")
+ self._complete_upto_grand_calculated_summary_cross_section_dependencies()
+
+ # don't skip calculated summary, confirm it, and go to section summary
+ self.post({"skip-answer-2": "No"})
+ self.post()
+ self.post()
+
+ # grand calculated summary will now include the previous calculated summary
+ self.post()
+ self.assertInBody(
+ "The grand calculated summary is calculated to be £160.00. Is this correct?"
+ )
+
+ def test_grand_calculated_summary_cross_section_dependencies_extra_question(self):
+ self.launchSurvey("test_grand_calculated_summary_cross_section_dependencies")
+ self._complete_upto_grand_calculated_summary_cross_section_dependencies()
+
+ # edit question to unlock the extra one
+ self.previous()
+ self.post(
+ {"third-number-answer-part-a": "70", "third-number-answer-part-b": "20"}
+ )
+ self.post({"fourth-number-answer": "40"})
+ self.post({"skip-answer-2": "No"})
+ self.post()
+ self.post()
+
+ # grand calculated summary will now include the extra question answer
+ self.post()
+ self.assertInBody(
+ "The grand calculated summary is calculated to be £220.00. Is this correct?"
+ )
+
+ def _complete_upto_grand_calculated_summary_overlapping_answers(
+ self, radio_answer: str
+ ):
+ self.post()
+ self.post()
+ self.post({"q1-a1": "100", "q1-a2": "200"})
+ self.post({"q2-a1": "10", "q2-a2": "20"})
+ self.post()
+ self.post()
+ self.post({"radio-extra": radio_answer})
+ if radio_answer != "No":
+ # in the no overlap case, the calculated summary is skipped entirely
+ self.post()
+ self.post()
+ self.post()
+
+ def test_grand_calculated_summary_overlapping_answers_full_overlap(self):
+ self.launchSurvey("test_grand_calculated_summary_overlapping_answers")
+ self._complete_upto_grand_calculated_summary_overlapping_answers(
+ "Yes, I am going to buy two of everything"
+ )
+ self.assertInBody(
+ "Grand Calculated Summary of purchases this week comes to £660.00. Is this correct?"
+ )
+
+ def test_grand_calculated_summary_overlapping_answers_partial_overlap(self):
+ self.launchSurvey("test_grand_calculated_summary_overlapping_answers")
+ self._complete_upto_grand_calculated_summary_overlapping_answers(
+ "Yes, extra bread and cheese"
+ )
+ self.assertInBody(
+ "Grand Calculated Summary of purchases this week comes to £360.00. Is this correct?"
+ )
+
+ def test_grand_calculated_summary_overlapping_answers_no_overlap(self):
+ self.launchSurvey("test_grand_calculated_summary_overlapping_answers")
+ self._complete_upto_grand_calculated_summary_overlapping_answers("No")
+ self.assertInBody(
+ "Grand Calculated Summary of purchases this week comes to £330.00. Is this correct?"
+ )
From d5f71732c774f053c6732b29a2395c8cc0f1ccee Mon Sep 17 00:00:00 2001
From: petechd <53475968+petechd@users.noreply.github.com>
Date: Tue, 6 Jun 2023 15:49:22 +0100
Subject: [PATCH 11/23] Fix dynamic answers functional test (#1121)
---
tests/functional/generate_pages.py | 18 ++
.../dynamic_answers_list_value_source.spec.js | 215 +++++++++---------
2 files changed, 122 insertions(+), 111 deletions(-)
diff --git a/tests/functional/generate_pages.py b/tests/functional/generate_pages.py
index 0b5910ae9b..d55a82741e 100755
--- a/tests/functional/generate_pages.py
+++ b/tests/functional/generate_pages.py
@@ -180,6 +180,14 @@
"""
)
+QUESTION_LABELS_GETTER = r""" labels() { return `.ons-label`; }
+
+"""
+
+QUESTION_INPUTS_GETTER = r""" inputs() { return `[data-qa="input-text"]`; }
+
+"""
+
DYNAMIC_ANSWER_GETTER = Template(
r""" answerByIndex(answerIndex) {
return `#${answerId}-${answerIndex}`;
@@ -222,6 +230,12 @@
"""
)
+SUMMARY_GROUP_GETTER = Template(
+ r""" ${group_id_camel}Content(groupNumber) { return `#${group_id_without_number}-` + groupNumber; }
+
+"""
+)
+
SUMMARY_QUESTION_GETTER = Template(
r""" ${questionName}() { return `[data-qa=${questionId}]`; }
@@ -491,6 +505,8 @@ def process_question(question, page_spec, num_questions, page_name):
page_spec.write(QUESTION_TITLE.substitute(question_context))
page_spec.write(ANSWER_NUMBERED_ERROR_LIST_GETTER)
page_spec.write(ANSWER_SINGLE_ERROR_LINK_GETTER)
+ page_spec.write(QUESTION_LABELS_GETTER)
+ page_spec.write(QUESTION_INPUTS_GETTER)
def process_calculated_summary(answers, page_spec):
@@ -657,8 +673,10 @@ def write_summary_spec(
group_context = {
"group_id_camel": camel_case(generate_pascal_case_from_id(group["id"])),
"group_id": f'{group["id"]}-0',
+ "group_id_without_number": f'{group["id"]}',
}
page_spec.write(SUMMARY_TITLE_GETTER.substitute(group_context))
+ page_spec.write(SUMMARY_GROUP_GETTER.substitute(group_context))
def long_names_required(question, num_questions):
diff --git a/tests/functional/spec/features/dynamic_answers_list_value_source.spec.js b/tests/functional/spec/features/dynamic_answers_list_value_source.spec.js
index de81a2f98a..c024ca0667 100644
--- a/tests/functional/spec/features/dynamic_answers_list_value_source.spec.js
+++ b/tests/functional/spec/features/dynamic_answers_list_value_source.spec.js
@@ -8,22 +8,19 @@ import SetMinimumPage from "../../generated_pages/dynamic_answers_list_source/mi
import SectionSummaryPage from "../../generated_pages/dynamic_answers_list_source/section-summary.page";
describe("Dynamic answers list value source", () => {
- const labels = 'label[class="ons-label"]';
- const percentageInputs = 'input[class="ons-input ons-input--text ons-input-type__input ons-input-number--w-3"]';
- const numberInputs = 'input[class="ons-input ons-input--text ons-input-type__input ons-input-number--w-1"]';
- const group = 'div[id="group-2"]';
- const summaryTitles = 'dt[class="ons-summary__item-title"]';
- const summaryValues = 'dd[class="ons-summary__values"]';
- const summaryActions = 'dd[class="ons-summary__actions"]';
+ const summaryTitles = ".ons-summary__item-title";
+ const summaryValues = ".ons-summary__values";
+ const summaryActions = ".ons-summary__actions";
+ const timeout = 2000;
beforeEach("Load the survey", async () => {
await browser.openQuestionnaire("test_dynamic_answers_list_source.json");
});
it("Given list items have been added, When the dynamic answers are displayed, Then the correct answers should be visible", async () => {
- await addTwoSupermarkets();
- await expect(await $$(labels)[0].getText()).to.equal("Percentage of shopping at Tesco");
- await expect(await $$(labels)[1].getText()).to.equal("Percentage of shopping at Aldi");
- await expect(await $$(labels).length).to.equal(4);
+ await addTwoSupermarkets(timeout);
+ await expect(await $$(DynamicAnswerPage.labels())[0].getText()).to.equal("Percentage of shopping at Tesco");
+ await expect(await $$(DynamicAnswerPage.labels())[1].getText()).to.equal("Percentage of shopping at Aldi");
+ await expect(await $$(DynamicAnswerPage.labels()).length).to.equal(4);
});
it("Given list items have been added, When additional items are added using add link, Then the correct dynamic answers are displayed", async () => {
await $(DriverPage.yes()).click();
@@ -33,113 +30,102 @@ describe("Dynamic answers list value source", () => {
await $(ListCollectorAddPage.submit()).click();
await $(ListCollectorPage.no()).click();
await $(ListCollectorPage.submit()).click();
- await expect(await $$(labels)[0].getText()).to.equal("Percentage of shopping at Tesco");
- await expect(await $$(labels).length).to.equal(2);
- await $(DynamicAnswerPage.submit()).click();
- await $(SetMinimumPage.setMinimum()).setValue(2);
- await $(SetMinimumPage.submit()).click();
- await $(DynamicAnswerOnlyPage.submit()).click();
+ await $(DynamicAnswerPage.labels()).waitForExist({ timeout: timeout });
+ await expect(await $$(DynamicAnswerPage.labels())[0].getText()).to.equal("Percentage of shopping at Tesco");
+ await expect(await $$(DynamicAnswerPage.labels()).length).to.equal(2);
+ await setMinimumAndGetSectionSummary(timeout);
await $(SectionSummaryPage.supermarketsListAddLink()).click();
await $(ListCollectorAddPage.supermarketName()).setValue("Aldi");
await $(ListCollectorAddPage.setMaximum()).setValue(10000);
await $(ListCollectorAddPage.submit()).click();
await $(ListCollectorPage.no()).click();
await $(ListCollectorPage.submit()).click();
- await expect(await $$(labels)[0].getText()).to.equal("Percentage of shopping at Tesco");
- await expect(await $$(labels)[1].getText()).to.equal("Percentage of shopping at Aldi");
- await expect(await $$(labels).length).to.equal(4);
+ await $(DynamicAnswerPage.inputs()).waitForExist({ timeout: timeout });
+ await expect(await $$(DynamicAnswerPage.labels())[0].getText()).to.equal("Percentage of shopping at Tesco");
+ await expect(await $$(DynamicAnswerPage.labels())[1].getText()).to.equal("Percentage of shopping at Aldi");
+ await expect(await $$(DynamicAnswerPage.labels()).length).to.equal(4);
});
it("Given list items have been added and the dynamic answers are submitted, When the summary is displayed, Then the correct answers should be visible and have correct values", async () => {
- await addTwoSupermarkets();
- await $$(percentageInputs)[0].setValue(12);
- await $$(percentageInputs)[1].setValue(21);
- await $$(numberInputs)[0].setValue(3);
- await $$(numberInputs)[1].setValue(7);
- await $(DynamicAnswerPage.submit()).click();
- await $(SetMinimumPage.setMinimum()).setValue(2);
- await $(SetMinimumPage.submit()).click();
- await $(DynamicAnswerOnlyPage.submit()).click();
- await expect(await $(group).$$(summaryTitles)[0].getText()).to.equal("Percentage of shopping at Tesco");
- await expect(await $(group).$$(summaryValues)[0].getText()).to.equal("12%");
- await expect(await $(group).$$(summaryTitles)[1].getText()).to.equal("Percentage of shopping at Aldi");
- await expect(await $(group).$$(summaryValues)[1].getText()).to.equal("21%");
- await expect(await $(group).$$(summaryValues)[2].getText()).to.equal("3");
- await expect(await $(group).$$(summaryValues)[3].getText()).to.equal("7");
- await expect(await $(group).$$(summaryTitles).length).to.equal(8);
- await expect(await $(group).$$(summaryValues).length).to.equal(8);
+ await addTwoSupermarkets(timeout);
+ await $$(DynamicAnswerPage.inputs())[0].setValue(12);
+ await $$(DynamicAnswerPage.inputs())[1].setValue(21);
+ await $$(DynamicAnswerPage.inputs())[2].setValue(3);
+ await $$(DynamicAnswerPage.inputs())[3].setValue(7);
+ await setMinimumAndGetSectionSummary(timeout);
+ await $(SectionSummaryPage.groupContent(2)).waitForExist({ timeout: timeout });
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles)[0].getText()).to.equal("Percentage of shopping at Tesco");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[0].getText()).to.equal("12%");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles)[1].getText()).to.equal("Percentage of shopping at Aldi");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[1].getText()).to.equal("21%");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[2].getText()).to.equal("3");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[3].getText()).to.equal("7");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles).length).to.equal(8);
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues).length).to.equal(8);
});
it("Given list items have been added and the dynamic answers are submitted, When the dynamic answers are revisited, Then they should be visible and have correct values", async () => {
- await addTwoSupermarkets();
- await $$(percentageInputs)[0].setValue(12);
- await $$(percentageInputs)[1].setValue(21);
- await $(DynamicAnswerPage.submit()).click();
- await $(SetMinimumPage.setMinimum()).setValue(2);
- await $(SetMinimumPage.submit()).click();
- await $(DynamicAnswerOnlyPage.submit()).click();
+ await addTwoSupermarkets(timeout);
+ await $$(DynamicAnswerPage.inputs())[0].setValue(12);
+ await $$(DynamicAnswerPage.inputs())[1].setValue(21);
+ await setMinimumAndGetSectionSummary(timeout);
await $(SectionSummaryPage.previous()).click();
await $(DynamicAnswerOnlyPage.previous()).click();
await $(SetMinimumPage.previous()).click();
await expect(await browser.getUrl()).to.contain(DynamicAnswerPage.pageName);
- await expect(await $$(percentageInputs)[0].getValue()).to.equal("12");
- await expect(await $$(percentageInputs)[1].getValue()).to.equal("21");
- await expect(await $$(labels)[0].getText()).to.equal("Percentage of shopping at Tesco");
- await expect(await $$(labels)[1].getText()).to.equal("Percentage of shopping at Aldi");
+ await $(DynamicAnswerPage.inputs()).waitForExist({ timeout: timeout });
+ await $(DynamicAnswerPage.labels()).waitForExist({ timeout: timeout });
+ await expect(await $$(DynamicAnswerPage.inputs())[0].getValue()).to.equal("12");
+ await expect(await $$(DynamicAnswerPage.inputs())[1].getValue()).to.equal("21");
+ await expect(await $$(DynamicAnswerPage.labels())[0].getText()).to.equal("Percentage of shopping at Tesco");
+ await expect(await $$(DynamicAnswerPage.labels())[1].getText()).to.equal("Percentage of shopping at Aldi");
});
it("Given list items have been added and the dynamic answers are submitted, When the dynamic answers are resubmitted with different values, Then they should be displayed correctly on summary", async () => {
- await addTwoSupermarkets();
- await $$(percentageInputs)[0].setValue(12);
- await $$(percentageInputs)[1].setValue(21);
- await $(DynamicAnswerPage.submit()).click();
- await $(SetMinimumPage.setMinimum()).setValue(2);
- await $(SetMinimumPage.submit()).click();
- await $(DynamicAnswerOnlyPage.submit()).click();
+ await addTwoSupermarkets(timeout);
+ await $$(DynamicAnswerPage.inputs())[0].setValue(12);
+ await $$(DynamicAnswerPage.inputs())[1].setValue(21);
+ await setMinimumAndGetSectionSummary(timeout);
await $(SectionSummaryPage.previous()).click();
await $(DynamicAnswerOnlyPage.previous()).click();
await $(SetMinimumPage.previous()).click();
- await $$(percentageInputs)[0].setValue(21);
- await $$(percentageInputs)[1].setValue(12);
+ await $$(DynamicAnswerPage.inputs())[0].waitForExist({ timeout: timeout });
+ await $$(DynamicAnswerPage.inputs())[0].setValue(21);
+ await $$(DynamicAnswerPage.inputs())[1].setValue(12);
await $(DynamicAnswerPage.submit()).click();
- await expect(await $(group).$$(summaryValues)[0].getText()).to.equal("21%");
- await expect(await $(group).$$(summaryValues)[1].getText()).to.equal("12%");
+ await $(SectionSummaryPage.groupContent(2)).waitForExist({ timeout: timeout });
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[0].getText()).to.equal("21%");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[1].getText()).to.equal("12%");
});
it("Given list items have been added and the dynamic answers are submitted, When the summary edit answer link is used for dynamic answer, Then the focus is on correct answer option", async () => {
- await addTwoSupermarkets();
- await $$(percentageInputs)[0].setValue(12);
- await $$(percentageInputs)[1].setValue(21);
- await $(DynamicAnswerPage.submit()).click();
- await $(SetMinimumPage.setMinimum()).setValue(2);
- await $(SetMinimumPage.submit()).click();
- await $(DynamicAnswerOnlyPage.submit()).click();
- await $(group).$$(summaryActions)[0].$("a").click();
+ await addTwoSupermarkets(timeout);
+ await $$(DynamicAnswerPage.inputs())[0].setValue(12);
+ await $$(DynamicAnswerPage.inputs())[1].setValue(21);
+ await setMinimumAndGetSectionSummary(timeout);
+ await $(SectionSummaryPage.groupContent(2)).$$(summaryActions)[0].$("a").click();
await expect(await browser.getUrl()).to.contain(DynamicAnswerPage.pageName);
- await expect(await $$(percentageInputs)[0].isFocused()).to.be.true;
+ await expect(await $$(DynamicAnswerPage.inputs())[0].isFocused()).to.be.true;
await $(DynamicAnswerPage.submit()).click();
- await $(group).$$(summaryActions)[1].$("a").click();
+ await $(SectionSummaryPage.groupContent(2)).waitForExist({ timeout: timeout });
+ await $(SectionSummaryPage.groupContent(2)).$$(summaryActions)[1].$("a").click();
await expect(await browser.getUrl()).to.contain(DynamicAnswerPage.pageName);
- await expect(await $$(percentageInputs)[1].isFocused()).to.be.true;
+ await expect(await $$(DynamicAnswerPage.inputs())[1].isFocused()).to.be.true;
});
it("Given list items have been added and the dynamic answers are submitted, When the dynamic answers are resubmitted with answers updated, Then they should be displayed correctly on summary", async () => {
- await addTwoSupermarkets();
- await $$(percentageInputs)[0].setValue(12);
- await $$(percentageInputs)[1].setValue(21);
+ await addTwoSupermarkets(timeout);
+ await $$(DynamicAnswerPage.inputs())[0].setValue(12);
+ await $$(DynamicAnswerPage.inputs())[1].setValue(21);
+ await setMinimumAndGetSectionSummary(timeout);
+ await $(SectionSummaryPage.groupContent(2)).$$(summaryActions)[0].$("a").click();
+ await $$(DynamicAnswerPage.inputs())[0].setValue(21);
await $(DynamicAnswerPage.submit()).click();
- await $(SetMinimumPage.setMinimum()).setValue(2);
- await $(SetMinimumPage.submit()).click();
- await $(DynamicAnswerOnlyPage.submit()).click();
- await $(group).$$(summaryActions)[0].$("a").click();
- await $$(percentageInputs)[0].setValue(21);
- await $(DynamicAnswerPage.submit()).click();
- await expect(await $(group).$$(summaryValues)[0].getText()).to.equal("21%");
- await expect(await $(group).$$(summaryValues)[1].getText()).to.equal("21%");
+ await $(SectionSummaryPage.groupContent(2)).waitForExist({ timeout: timeout });
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[0].getText()).to.equal("21%");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[1].getText()).to.equal("21%");
});
it("Given list items have been added and the dynamic answers are submitted, When the list items are removed and answers updated, Then they should be displayed correctly on summary", async () => {
- await addTwoSupermarkets();
- await $$(percentageInputs)[0].setValue(12);
- await $$(percentageInputs)[1].setValue(21);
- await $(DynamicAnswerPage.submit()).click();
- await $(SetMinimumPage.setMinimum()).setValue(2);
- await $(SetMinimumPage.submit()).click();
- await $(DynamicAnswerOnlyPage.submit()).click();
+ await addTwoSupermarkets(timeout);
+ await $$(DynamicAnswerPage.inputs())[0].setValue(12);
+ await $$(DynamicAnswerPage.inputs())[1].setValue(21);
+ await setMinimumAndGetSectionSummary(timeout);
+ await $(SectionSummaryPage.supermarketsListRemoveLink(1)).waitForExist({ timeout: timeout });
await $(SectionSummaryPage.supermarketsListRemoveLink(1)).click();
await $(ListCollectorRemovePage.yes()).click();
await $(ListCollectorRemovePage.submit()).click();
@@ -148,44 +134,42 @@ describe("Dynamic answers list value source", () => {
await $(SetMinimumPage.submit()).click();
await $(DynamicAnswerOnlyPage.submit()).click();
await expect(await browser.getUrl()).to.contain(SectionSummaryPage.pageName);
- await expect(await $(group).$$(summaryTitles)[0].getText()).to.equal("Percentage of shopping at Aldi");
- await expect(await $(group).$$(summaryValues)[0].getText()).to.equal("21%");
- await expect(await $(group).$$(summaryTitles).length).to.equal(5);
- await expect(await $(group).$$(summaryValues).length).to.equal(5);
+ await $(SectionSummaryPage.groupContent(2)).waitForExist({ timeout: timeout });
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles)[0].getText()).to.equal("Percentage of shopping at Aldi");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[0].getText()).to.equal("21%");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles).length).to.equal(5);
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues).length).to.equal(5);
});
it("Given list items have been added and the dynamic answers are submitted, When the driving question is changed to 'No', Then after changing answer to 'Yes' all answers should re-appear on summary", async () => {
- await addTwoSupermarkets();
- await $$(percentageInputs)[0].setValue(12);
- await $$(percentageInputs)[1].setValue(21);
- await $$(numberInputs)[0].setValue(3);
- await $$(numberInputs)[1].setValue(7);
- await $(DynamicAnswerPage.submit()).click();
- await $(SetMinimumPage.setMinimum()).setValue(2);
- await $(SetMinimumPage.submit()).click();
- await $(DynamicAnswerOnlyPage.submit()).click();
+ await addTwoSupermarkets(timeout);
+ await $$(DynamicAnswerPage.inputs())[0].setValue(12);
+ await $$(DynamicAnswerPage.inputs())[1].setValue(21);
+ await $$(DynamicAnswerPage.inputs())[2].setValue(3);
+ await $$(DynamicAnswerPage.inputs())[3].setValue(7);
+ await setMinimumAndGetSectionSummary(timeout);
await $(SectionSummaryPage.anySupermarketAnswerEdit()).click();
await $(DriverPage.no()).click();
await $(DriverPage.submit()).click();
await expect(await $(SectionSummaryPage.supermarketsListEditLink(1)).isExisting()).to.be.false;
await expect(await $(SectionSummaryPage.supermarketsListAddLink()).isExisting()).to.be.false;
- await expect(await $(group).isExisting()).to.be.false;
await $(SectionSummaryPage.anySupermarketAnswerEdit()).click();
await $(DriverPage.yes()).click();
await $(DriverPage.submit()).click();
+ await $(SectionSummaryPage.groupContent(2)).waitForExist({ timeout: timeout });
await expect(await $(SectionSummaryPage.supermarketsListEditLink(1)).isExisting()).to.be.true;
await expect(await $(SectionSummaryPage.supermarketsListAddLink()).isExisting()).to.be.true;
- await expect(await $(group).$$(summaryTitles)[0].getText()).to.equal("Percentage of shopping at Tesco");
- await expect(await $(group).$$(summaryValues)[0].getText()).to.equal("12%");
- await expect(await $(group).$$(summaryTitles)[1].getText()).to.equal("Percentage of shopping at Aldi");
- await expect(await $(group).$$(summaryValues)[1].getText()).to.equal("21%");
- await expect(await $(group).$$(summaryValues)[2].getText()).to.equal("3");
- await expect(await $(group).$$(summaryValues)[3].getText()).to.equal("7");
- await expect(await $(group).$$(summaryTitles).length).to.equal(8);
- await expect(await $(group).$$(summaryValues).length).to.equal(8);
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles)[0].getText()).to.equal("Percentage of shopping at Tesco");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[0].getText()).to.equal("12%");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles)[1].getText()).to.equal("Percentage of shopping at Aldi");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[1].getText()).to.equal("21%");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[2].getText()).to.equal("3");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues)[3].getText()).to.equal("7");
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles).length).to.equal(8);
+ await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryValues).length).to.equal(8);
});
});
-async function addTwoSupermarkets() {
+async function addTwoSupermarkets(timeout) {
await $(DriverPage.yes()).click();
await $(DriverPage.submit()).click();
await $(ListCollectorAddPage.supermarketName()).setValue("Tesco");
@@ -198,4 +182,13 @@ async function addTwoSupermarkets() {
await $(ListCollectorAddPage.submit()).click();
await $(ListCollectorPage.no()).click();
await $(ListCollectorPage.submit()).click();
+ await $(DynamicAnswerPage.inputs()).waitForExist({ timeout: timeout });
+}
+
+async function setMinimumAndGetSectionSummary(timeout) {
+ await $(DynamicAnswerPage.submit()).click();
+ await $(SetMinimumPage.setMinimum()).setValue(2);
+ await $(SetMinimumPage.submit()).click();
+ await $(DynamicAnswerOnlyPage.submit()).click();
+ await $(SectionSummaryPage.groupContent(2)).waitForExist({ timeout: timeout });
}
From ab1967248c042ee13602dcbe963083b2c699b0c4 Mon Sep 17 00:00:00 2001
From: Rhys Berrow <47635349+berroar@users.noreply.github.com>
Date: Wed, 7 Jun 2023 12:09:45 +0100
Subject: [PATCH 12/23] Schemas v3.59.0 (#1130)
---
.schemas-version | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.schemas-version b/.schemas-version
index 6d07408b02..e968ff7420 100644
--- a/.schemas-version
+++ b/.schemas-version
@@ -1 +1 @@
-v3.58.0
+v3.59.0
From a2986c0332d8ec996b00c2fd720954aa2f92a099 Mon Sep 17 00:00:00 2001
From: Rhys Berrow <47635349+berroar@users.noreply.github.com>
Date: Thu, 8 Jun 2023 13:45:19 +0100
Subject: [PATCH 13/23] Schemas v3.60.0 (#1133)
---
.schemas-version | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.schemas-version b/.schemas-version
index e968ff7420..ed4d98134d 100644
--- a/.schemas-version
+++ b/.schemas-version
@@ -1 +1 @@
-v3.59.0
+v3.60.0
From 090e6529ca77b124c1c73172b86dc2b09fb17855 Mon Sep 17 00:00:00 2001
From: Rhys Berrow <47635349+berroar@users.noreply.github.com>
Date: Fri, 9 Jun 2023 15:16:29 +0100
Subject: [PATCH 14/23] Update to chromedriver v114 (#1134)
---
package.json | 2 +-
yarn.lock | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index 5e7eb261de..589f4068c3 100644
--- a/package.json
+++ b/package.json
@@ -33,7 +33,7 @@
"@wdio/mocha-framework": "^8.3.0",
"@wdio/spec-reporter": "^8.3.0",
"chai": "^4.3.6",
- "chromedriver": "^113.0.0",
+ "chromedriver": "^114.0.0",
"eslint": "^8.10.0",
"eslint-cli": "^1.1.1",
"eslint-config-standard": "^14.1.1",
diff --git a/yarn.lock b/yarn.lock
index 724845cce5..ebd0b67798 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1786,10 +1786,10 @@ chrome-launcher@^0.15.0:
is-wsl "^2.2.0"
lighthouse-logger "^1.0.0"
-chromedriver@^113.0.0:
- version "113.0.0"
- resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-113.0.0.tgz#d4855f156ee51cea4282e04aadd29fa154e44dbb"
- integrity sha512-UnQlt2kPicYXVNHPzy9HfcWvEbKJjjKAEaatdcnP/lCIRwuSoZFVLH0HVDAGdbraXp3dNVhfE2Qx7gw8TnHnPw==
+chromedriver@^114.0.0:
+ version "114.0.1"
+ resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-114.0.1.tgz#6b781922e74e2f6520cbd3e363dfabc41c4347f5"
+ integrity sha512-Srkyt7xv+RL9aSNVkmARm0tAfw84fIBKge9c1MCTiHfW0tjuNFdhKVlgD0TmPmwSKOeFJrTdd1Flf2hGWWKsUw==
dependencies:
"@testim/chrome-version" "^1.1.3"
axios "^1.2.1"
From 1dcf278e9943acab6fea31361120f71a92bc97be Mon Sep 17 00:00:00 2001
From: petechd <53475968+petechd@users.noreply.github.com>
Date: Mon, 12 Jun 2023 12:31:22 +0100
Subject: [PATCH 15/23] Add DESNZ theme (#1131)
---
app/helpers/template_helpers.py | 4 +
app/survey_config/__init__.py | 4 +
app/survey_config/business_config.py | 12 ++
app/survey_config/survey_type.py | 2 +
schemas/test/en/test_theme_desnz.json | 75 +++++++++++
schemas/test/en/test_theme_desnz_ni.json | 75 +++++++++++
.../assets/images/desnz-logo-stacked.svg | 116 ++++++++++++++++++
templates/errors/_base.html | 2 +-
tests/app/helpers/test_template_helpers.py | 73 +++++++++++
tests/functional/spec/theme_desnz.spec.js | 14 +++
tests/functional/spec/theme_desnz_ni.spec.js | 15 +++
11 files changed, 391 insertions(+), 1 deletion(-)
create mode 100644 schemas/test/en/test_theme_desnz.json
create mode 100644 schemas/test/en/test_theme_desnz_ni.json
create mode 100644 templates/assets/images/desnz-logo-stacked.svg
create mode 100644 tests/functional/spec/theme_desnz.spec.js
create mode 100644 tests/functional/spec/theme_desnz_ni.spec.js
diff --git a/app/helpers/template_helpers.py b/app/helpers/template_helpers.py
index 10f82a2be9..4a5e8ec402 100644
--- a/app/helpers/template_helpers.py
+++ b/app/helpers/template_helpers.py
@@ -21,6 +21,8 @@
DBTDSITBusinessSurveyConfig,
DBTDSITNIBusinessSurveyConfig,
DBTNIBusinessSurveyConfig,
+ DESNZBusinessSurveyConfig,
+ DESNZNIBusinessSurveyConfig,
NIBusinessSurveyConfig,
ORRBusinessSurveyConfig,
SocialSurveyConfig,
@@ -188,6 +190,8 @@ def survey_config_mapping(
SurveyType.DBT_NI: DBTNIBusinessSurveyConfig,
SurveyType.DBT_DSIT: DBTDSITBusinessSurveyConfig,
SurveyType.DBT_DSIT_NI: DBTDSITNIBusinessSurveyConfig,
+ SurveyType.DESNZ: DESNZBusinessSurveyConfig,
+ SurveyType.DESNZ_NI: DESNZNIBusinessSurveyConfig,
SurveyType.ORR: ORRBusinessSurveyConfig,
SurveyType.CENSUS: (
WelshCensusSurveyConfig if language == "cy" else CensusSurveyConfig
diff --git a/app/survey_config/__init__.py b/app/survey_config/__init__.py
index c0f4f9b481..36393c2636 100644
--- a/app/survey_config/__init__.py
+++ b/app/survey_config/__init__.py
@@ -4,6 +4,8 @@
DBTDSITBusinessSurveyConfig,
DBTDSITNIBusinessSurveyConfig,
DBTNIBusinessSurveyConfig,
+ DESNZBusinessSurveyConfig,
+ DESNZNIBusinessSurveyConfig,
NIBusinessSurveyConfig,
ORRBusinessSurveyConfig,
)
@@ -29,5 +31,7 @@
"DBTDSITBusinessSurveyConfig",
"DBTDSITNIBusinessSurveyConfig",
"ORRBusinessSurveyConfig",
+ "DESNZBusinessSurveyConfig",
+ "DESNZNIBusinessSurveyConfig",
"Link",
]
diff --git a/app/survey_config/business_config.py b/app/survey_config/business_config.py
index c9803a33f8..8019845eea 100644
--- a/app/survey_config/business_config.py
+++ b/app/survey_config/business_config.py
@@ -157,6 +157,18 @@ class DBTNIBusinessSurveyConfig(BusinessSurveyConfig):
) + read_file("./templates/assets/images/finance-ni-logo-stacked.svg")
+@dataclass
+class DESNZBusinessSurveyConfig(BusinessSurveyConfig):
+ masthead_logo: str = read_file("./templates/assets/images/desnz-logo-stacked.svg")
+
+
+@dataclass
+class DESNZNIBusinessSurveyConfig(BusinessSurveyConfig):
+ masthead_logo: str = read_file(
+ "./templates/assets/images/desnz-logo-stacked.svg"
+ ) + read_file("./templates/assets/images/finance-ni-logo-stacked.svg")
+
+
@dataclass
class ORRBusinessSurveyConfig(BusinessSurveyConfig):
masthead_logo: str = read_file("./templates/assets/images/orr-logo.svg")
diff --git a/app/survey_config/survey_type.py b/app/survey_config/survey_type.py
index 01098e2470..ae3b3d978a 100644
--- a/app/survey_config/survey_type.py
+++ b/app/survey_config/survey_type.py
@@ -12,5 +12,7 @@ class SurveyType(Enum):
DBT_DSIT = "dbt-dsit"
DBT_DSIT_NI = "dbt-dsit-ni"
ORR = "orr"
+ DESNZ = "desnz"
+ DESNZ_NI = "desnz-ni"
CENSUS = "census"
CENSUS_NISRA = "census-nisra"
diff --git a/schemas/test/en/test_theme_desnz.json b/schemas/test/en/test_theme_desnz.json
new file mode 100644
index 0000000000..c1aa83755f
--- /dev/null
+++ b/schemas/test/en/test_theme_desnz.json
@@ -0,0 +1,75 @@
+{
+ "mime_type": "application/json/ons/eq",
+ "language": "en",
+ "schema_version": "0.0.1",
+ "data_version": "0.0.3",
+ "survey_id": "0",
+ "title": "Test Department for Energy Security and Net Zero",
+ "theme": "desnz",
+ "description": "A questionnaire to demo the DESNZ survey theme",
+ "metadata": [
+ {
+ "name": "user_id",
+ "type": "string"
+ },
+ {
+ "name": "period_id",
+ "type": "string"
+ },
+ {
+ "name": "ru_name",
+ "type": "string"
+ }
+ ],
+ "questionnaire_flow": {
+ "type": "Linear",
+ "options": {
+ "summary": {
+ "collapsible": false
+ }
+ }
+ },
+ "sections": [
+ {
+ "id": "section",
+ "groups": [
+ {
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "radio",
+ "question": {
+ "answers": [
+ {
+ "id": "radio-answer",
+ "mandatory": false,
+ "options": [
+ {
+ "label": "Bacon",
+ "value": "Bacon"
+ },
+ {
+ "label": "Eggs",
+ "value": "Eggs"
+ },
+ {
+ "label": "Sausage",
+ "value": "Sausage"
+ }
+ ],
+ "type": "Radio"
+ }
+ ],
+ "id": "radio-question",
+ "title": "What is your favourite breakfast food?",
+ "type": "General"
+ }
+ }
+ ],
+ "id": "group",
+ "title": "DESNZ Theme Test"
+ }
+ ]
+ }
+ ]
+}
diff --git a/schemas/test/en/test_theme_desnz_ni.json b/schemas/test/en/test_theme_desnz_ni.json
new file mode 100644
index 0000000000..06751903f5
--- /dev/null
+++ b/schemas/test/en/test_theme_desnz_ni.json
@@ -0,0 +1,75 @@
+{
+ "mime_type": "application/json/ons/eq",
+ "language": "en",
+ "schema_version": "0.0.1",
+ "data_version": "0.0.3",
+ "survey_id": "0",
+ "title": "Test NI Department for Energy Security and Net Zero",
+ "theme": "desnz-ni",
+ "description": "A questionnaire to demo the DESNZ-NI survey theme",
+ "metadata": [
+ {
+ "name": "user_id",
+ "type": "string"
+ },
+ {
+ "name": "period_id",
+ "type": "string"
+ },
+ {
+ "name": "ru_name",
+ "type": "string"
+ }
+ ],
+ "questionnaire_flow": {
+ "type": "Linear",
+ "options": {
+ "summary": {
+ "collapsible": false
+ }
+ }
+ },
+ "sections": [
+ {
+ "id": "section",
+ "groups": [
+ {
+ "blocks": [
+ {
+ "type": "Question",
+ "id": "radio",
+ "question": {
+ "answers": [
+ {
+ "id": "radio-answer",
+ "mandatory": false,
+ "options": [
+ {
+ "label": "Bacon",
+ "value": "Bacon"
+ },
+ {
+ "label": "Eggs",
+ "value": "Eggs"
+ },
+ {
+ "label": "Sausage",
+ "value": "Sausage"
+ }
+ ],
+ "type": "Radio"
+ }
+ ],
+ "id": "radio-question",
+ "title": "What is your favourite breakfast food?",
+ "type": "General"
+ }
+ }
+ ],
+ "id": "group",
+ "title": "DESNZ-NI Theme Test"
+ }
+ ]
+ }
+ ]
+}
diff --git a/templates/assets/images/desnz-logo-stacked.svg b/templates/assets/images/desnz-logo-stacked.svg
new file mode 100644
index 0000000000..ea62005183
--- /dev/null
+++ b/templates/assets/images/desnz-logo-stacked.svg
@@ -0,0 +1,116 @@
+
diff --git a/templates/errors/_base.html b/templates/errors/_base.html
index 41cdd3d9f6..06a5a47cc0 100644
--- a/templates/errors/_base.html
+++ b/templates/errors/_base.html
@@ -1,6 +1,6 @@
{% extends 'layouts/_base.html' %}
-{% set SURVEY_TYPES_BUSINESS = ["northernireland", "business", "dbt", "dbt-ni", "dbt-dsit", "dbt-dsit-ni", "orr"] %}
+{% set SURVEY_TYPES_BUSINESS = ["northernireland", "business", "dbt", "dbt-ni", "dbt-dsit", "dbt-dsit-ni", "orr", "desnz", "desnz-ni"] %}
{% set SURVEY_TYPES_DEFAULT = ["default"] %}
{% set SURVEY_TYPES_SOCIAL = ["social"] %}
{% set SURVEY_TYPES_HEALTH = ["health"] %}
diff --git a/tests/app/helpers/test_template_helpers.py b/tests/app/helpers/test_template_helpers.py
index 8039d8ad31..b7449148d8 100644
--- a/tests/app/helpers/test_template_helpers.py
+++ b/tests/app/helpers/test_template_helpers.py
@@ -22,6 +22,8 @@
DBTDSITBusinessSurveyConfig,
DBTDSITNIBusinessSurveyConfig,
DBTNIBusinessSurveyConfig,
+ DESNZBusinessSurveyConfig,
+ DESNZNIBusinessSurveyConfig,
NIBusinessSurveyConfig,
ORRBusinessSurveyConfig,
SocialSurveyConfig,
@@ -340,6 +342,27 @@ def test_footer_warning_not_in_context_census_theme(app: Flask):
read_file("./templates/assets/images/orr-mobile-logo.svg"),
],
),
+ (
+ SurveyType.DESNZ,
+ "Test",
+ DESNZBusinessSurveyConfig(),
+ [
+ "Test",
+ read_file("./templates/assets/images/desnz-logo-stacked.svg"),
+ None,
+ ],
+ ),
+ (
+ SurveyType.DESNZ,
+ "Test",
+ DESNZNIBusinessSurveyConfig(),
+ [
+ "Test",
+ read_file("./templates/assets/images/desnz-logo-stacked.svg")
+ + read_file("./templates/assets/images/finance-ni-logo-stacked.svg"),
+ None,
+ ],
+ ),
),
)
def test_header_context(app: Flask, theme, survey_title, survey_config, expected):
@@ -499,6 +522,16 @@ def test_service_links_context(
"en",
f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/",
),
+ (
+ DESNZBusinessSurveyConfig(),
+ "en",
+ f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/",
+ ),
+ (
+ DESNZNIBusinessSurveyConfig(),
+ "en",
+ f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/",
+ ),
(
SocialSurveyConfig(),
"en",
@@ -588,6 +621,16 @@ def test_sign_out_button_text_context(
True,
f"{ACCOUNT_SERVICE_BASE_URL}/cookies/",
),
+ (
+ DESNZBusinessSurveyConfig(),
+ True,
+ f"{ACCOUNT_SERVICE_BASE_URL}/cookies/",
+ ),
+ (
+ DESNZNIBusinessSurveyConfig(),
+ True,
+ f"{ACCOUNT_SERVICE_BASE_URL}/cookies/",
+ ),
(
SocialSurveyConfig(),
True,
@@ -657,6 +700,16 @@ def test_cookie_settings_url_context(
"en",
ACCOUNT_SERVICE_BASE_URL,
),
+ (
+ DESNZBusinessSurveyConfig(),
+ "en",
+ ACCOUNT_SERVICE_BASE_URL,
+ ),
+ (
+ DESNZNIBusinessSurveyConfig(),
+ "en",
+ ACCOUNT_SERVICE_BASE_URL,
+ ),
(
SocialSurveyConfig(),
"en",
@@ -792,6 +845,14 @@ def test_account_service_my_todo_url_context(
ORRBusinessSurveyConfig(),
f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout",
),
+ (
+ DESNZBusinessSurveyConfig(),
+ f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout",
+ ),
+ (
+ DESNZNIBusinessSurveyConfig(),
+ f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout",
+ ),
(
SocialSurveyConfig(),
f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/en/start/",
@@ -1061,6 +1122,18 @@ def test_use_default_survey_title_in_context_when_no_cookie(
QuestionnaireSchema({"survey_id": "001"}),
[{"survey_id": "001"}],
),
+ (
+ SurveyType.DESNZ,
+ "en",
+ QuestionnaireSchema({"survey_id": "001"}),
+ [{"survey_id": "001"}],
+ ),
+ (
+ SurveyType.DESNZ_NI,
+ "en",
+ QuestionnaireSchema({"survey_id": "001"}),
+ [{"survey_id": "001"}],
+ ),
(
SurveyType.CENSUS,
"en",
diff --git a/tests/functional/spec/theme_desnz.spec.js b/tests/functional/spec/theme_desnz.spec.js
new file mode 100644
index 0000000000..e67af59233
--- /dev/null
+++ b/tests/functional/spec/theme_desnz.spec.js
@@ -0,0 +1,14 @@
+import RadioPage from "../generated_pages/theme_desnz/radio.page";
+
+describe("Theme DESNZ", () => {
+ describe("Given I launch a DESNZ themed questionnaire", () => {
+ before(async () => {
+ await browser.openQuestionnaire("test_theme_desnz.json");
+ });
+
+ it("When I navigate to the radio page, Then I should see DESNZ theme content", async () => {
+ await expect(await browser.getUrl()).to.contain(RadioPage.pageName);
+ await expect(await $("#desnz-logo-alt").getHTML()).to.contain("Department for Energy Security and Net Zero");
+ });
+ });
+});
diff --git a/tests/functional/spec/theme_desnz_ni.spec.js b/tests/functional/spec/theme_desnz_ni.spec.js
new file mode 100644
index 0000000000..7896f46ef5
--- /dev/null
+++ b/tests/functional/spec/theme_desnz_ni.spec.js
@@ -0,0 +1,15 @@
+import RadioPage from "../generated_pages/theme_desnz_ni/radio.page";
+
+describe("Theme DESNZ-NI", () => {
+ describe("Given I launch a DESNZ-NI themed questionnaire", () => {
+ before(async () => {
+ await browser.openQuestionnaire("test_theme_desnz_ni.json");
+ });
+
+ it("When I navigate to the radio page, Then I should see DESNZ-NI theme content", async () => {
+ await expect(await browser.getUrl()).to.contain(RadioPage.pageName);
+ await expect(await $("#desnz-logo-alt").getHTML()).to.contain("Department for Energy Security and Net Zero");
+ await expect(await $("#finance-ni-logo-alt").getHTML()).to.contain("Northern Ireland Department of Finance logo");
+ });
+ });
+});
From d04325cdaa17ccde913098ecc86375341943da09 Mon Sep 17 00:00:00 2001
From: Rhys Berrow <47635349+berroar@users.noreply.github.com>
Date: Tue, 13 Jun 2023 15:33:47 +0100
Subject: [PATCH 16/23] Schemas v3.61.0 (#1139)
---
.schemas-version | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.schemas-version b/.schemas-version
index ed4d98134d..0fac7be0d1 100644
--- a/.schemas-version
+++ b/.schemas-version
@@ -1 +1 @@
-v3.60.0
+v3.61.0
From f5d1b7df3f2e379ad122cd10476f0b169578edbf Mon Sep 17 00:00:00 2001
From: petechd <53475968+petechd@users.noreply.github.com>
Date: Tue, 20 Jun 2023 13:57:22 +0100
Subject: [PATCH 17/23] Add schema changes for validator format_unit using
calculated summary source validation fix (#1142)
---
schemas/test/en/test_calculated_summary.json | 44 ++++++++++++++++++-
.../test/en/test_new_calculated_summary.json | 44 ++++++++++++++++++-
.../features/calculated_summary_test_case.js | 9 +++-
3 files changed, 92 insertions(+), 5 deletions(-)
diff --git a/schemas/test/en/test_calculated_summary.json b/schemas/test/en/test_calculated_summary.json
index 71753ae08d..199a993cff 100644
--- a/schemas/test/en/test_calculated_summary.json
+++ b/schemas/test/en/test_calculated_summary.json
@@ -345,7 +345,7 @@
]
},
{
- "text": "Total unit values: {unit_total}",
+ "text": "Total unformatted unit values: {unit_total}",
"placeholders": [
{
"placeholder": "unit_total",
@@ -364,7 +364,28 @@
]
},
{
- "text": "Total percentage values: {percentage_total}",
+ "text": "Total formatted unit values: {unit_total}",
+ "placeholders": [
+ {
+ "placeholder": "unit_total",
+ "transforms": [
+ {
+ "transform": "format_unit",
+ "arguments": {
+ "value": {
+ "source": "calculated_summary",
+ "identifier": "unit-total-playback"
+ },
+ "unit": "length-centimeter",
+ "unit_length": "short"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "text": "Total unformatted percentage values: {percentage_total}",
"placeholders": [
{
"placeholder": "percentage_total",
@@ -382,6 +403,25 @@
}
]
},
+ {
+ "text": "Total formatted percentage values: {percentage_total}",
+ "placeholders": [
+ {
+ "placeholder": "percentage_total",
+ "transforms": [
+ {
+ "transform": "format_percentage",
+ "arguments": {
+ "value": {
+ "source": "calculated_summary",
+ "identifier": "percentage-total-playback"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
{
"text": "Total number values: {number_total}",
"placeholders": [
diff --git a/schemas/test/en/test_new_calculated_summary.json b/schemas/test/en/test_new_calculated_summary.json
index 36afe23f8a..98b77580d7 100644
--- a/schemas/test/en/test_new_calculated_summary.json
+++ b/schemas/test/en/test_new_calculated_summary.json
@@ -394,7 +394,7 @@
]
},
{
- "text": "Total unit values: {unit_total}",
+ "text": "Total unformatted unit values: {unit_total}",
"placeholders": [
{
"placeholder": "unit_total",
@@ -413,7 +413,28 @@
]
},
{
- "text": "Total percentage values: {percentage_total}",
+ "text": "Total formatted unit values: {unit_total}",
+ "placeholders": [
+ {
+ "placeholder": "unit_total",
+ "transforms": [
+ {
+ "transform": "format_unit",
+ "arguments": {
+ "value": {
+ "source": "calculated_summary",
+ "identifier": "unit-total-playback"
+ },
+ "unit": "length-centimeter",
+ "unit_length": "short"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "text": "Total unformatted percentage values: {percentage_total}",
"placeholders": [
{
"placeholder": "percentage_total",
@@ -431,6 +452,25 @@
}
]
},
+ {
+ "text": "Total formatted percentage values: {percentage_total}",
+ "placeholders": [
+ {
+ "placeholder": "percentage_total",
+ "transforms": [
+ {
+ "transform": "format_percentage",
+ "arguments": {
+ "value": {
+ "source": "calculated_summary",
+ "identifier": "percentage-total-playback"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ },
{
"text": "Total number values: {number_total}",
"placeholders": [
diff --git a/tests/functional/spec/features/calculated_summary_test_case.js b/tests/functional/spec/features/calculated_summary_test_case.js
index fd088e7564..d4ee91e1af 100644
--- a/tests/functional/spec/features/calculated_summary_test_case.js
+++ b/tests/functional/spec/features/calculated_summary_test_case.js
@@ -346,7 +346,14 @@ class TestCase {
await browser.url(CalculatedSummaryTotalConfirmation.url());
await expect(await browser.getUrl()).to.contain(CalculatedSummaryTotalConfirmation.pageName);
const content = await $("h1 + ul").getText();
- const textsToAssert = ["Total currency values: £25.92", "Total unit values: 1,467", "Total percentage values: 79", "Total number values: 124.58"];
+ const textsToAssert = [
+ "Total currency values: £25.92",
+ "Total unformatted unit values: 1,467",
+ "Total formatted unit values: 1,467 cm",
+ "Total unformatted percentage values: 79",
+ "Total formatted percentage values: 79%",
+ "Total number values: 124.58",
+ ];
textsToAssert.forEach((text) => expect(content).to.contain(text));
await browser.url(SubmitPage.url());
From c0bb6bbd9226f1c838ded343276a1006d1bef5b9 Mon Sep 17 00:00:00 2001
From: Rhys Berrow <47635349+berroar@users.noreply.github.com>
Date: Tue, 20 Jun 2023 15:40:30 +0100
Subject: [PATCH 18/23] Schemas v3.62.0 (#1144)
---
.schemas-version | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.schemas-version b/.schemas-version
index 0fac7be0d1..5cd3892448 100644
--- a/.schemas-version
+++ b/.schemas-version
@@ -1 +1 @@
-v3.61.0
+v3.62.0
From 004c882c423a464b7b7c831674c044a94f2a1c9b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 23 Jun 2023 14:18:17 +0100
Subject: [PATCH 19/23] Bump cryptography from 40.0.2 to 41.0.0 (#1124)
---
Pipfile.lock | 369 +++++++++++++++++++++++++++------------------------
1 file changed, 193 insertions(+), 176 deletions(-)
diff --git a/Pipfile.lock b/Pipfile.lock
index 7f8c8b727d..3af7ed83cf 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -50,11 +50,11 @@
},
"botocore": {
"hashes": [
- "sha256:3005a7ffee083315e69938acdf1bfeaf9e21fe1fe1643d6573ee817721f4ffcd",
- "sha256:ac87b63e9aa4231cd28941945024a0c4470c184c60334ebe5e1cae3544c785ed"
+ "sha256:77f7793cb36074eb84d606a23ad6e1d57c20f7a2eeab7d9136d3e63c584e0504",
+ "sha256:ac57003292f18206ee942eafc381ecd9a3420a3844d6b7e1c1b0f4b88b28263b"
],
"markers": "python_version >= '3.7'",
- "version": "==1.29.125"
+ "version": "==1.29.146"
},
"brotli": {
"hashes": [
@@ -145,19 +145,19 @@
},
"cachetools": {
"hashes": [
- "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14",
- "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"
+ "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590",
+ "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"
],
- "markers": "python_version ~= '3.7'",
- "version": "==5.3.0"
+ "markers": "python_version >= '3.7'",
+ "version": "==5.3.1"
},
"certifi": {
"hashes": [
- "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
- "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
+ "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
+ "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
],
"markers": "python_version >= '3.6'",
- "version": "==2022.12.7"
+ "version": "==2023.5.7"
},
"cffi": {
"hashes": [
@@ -306,7 +306,7 @@
"sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df",
"sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"
],
- "markers": "python_full_version >= '3.7.0'",
+ "markers": "python_version >= '3.7'",
"version": "==3.1.0"
},
"click": {
@@ -335,43 +335,43 @@
},
"cryptography": {
"hashes": [
- "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440",
- "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288",
- "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b",
- "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958",
- "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b",
- "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d",
- "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a",
- "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404",
- "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b",
- "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e",
- "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2",
- "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c",
- "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b",
- "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9",
- "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b",
- "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636",
- "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99",
- "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e",
- "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==40.0.2"
+ "sha256:0ddaee209d1cf1f180f1efa338a68c4621154de0afaef92b89486f5f96047c55",
+ "sha256:14754bcdae909d66ff24b7b5f166d69340ccc6cb15731670435efd5719294895",
+ "sha256:344c6de9f8bda3c425b3a41b319522ba3208551b70c2ae00099c205f0d9fd3be",
+ "sha256:34d405ea69a8b34566ba3dfb0521379b210ea5d560fafedf9f800a9a94a41928",
+ "sha256:3680248309d340fda9611498a5319b0193a8dbdb73586a1acf8109d06f25b92d",
+ "sha256:3c5ef25d060c80d6d9f7f9892e1d41bb1c79b78ce74805b8cb4aa373cb7d5ec8",
+ "sha256:4ab14d567f7bbe7f1cdff1c53d5324ed4d3fc8bd17c481b395db224fb405c237",
+ "sha256:5c1f7293c31ebc72163a9a0df246f890d65f66b4a40d9ec80081969ba8c78cc9",
+ "sha256:6b71f64beeea341c9b4f963b48ee3b62d62d57ba93eb120e1196b31dc1025e78",
+ "sha256:7d92f0248d38faa411d17f4107fc0bce0c42cae0b0ba5415505df72d751bf62d",
+ "sha256:8362565b3835ceacf4dc8f3b56471a2289cf51ac80946f9087e66dc283a810e0",
+ "sha256:84a165379cb9d411d58ed739e4af3396e544eac190805a54ba2e0322feb55c46",
+ "sha256:88ff107f211ea696455ea8d911389f6d2b276aabf3231bf72c8853d22db755c5",
+ "sha256:9f65e842cb02550fac96536edb1d17f24c0a338fd84eaf582be25926e993dde4",
+ "sha256:a4fc68d1c5b951cfb72dfd54702afdbbf0fb7acdc9b7dc4301bbf2225a27714d",
+ "sha256:b7f2f5c525a642cecad24ee8670443ba27ac1fab81bba4cc24c7b6b41f2d0c75",
+ "sha256:b846d59a8d5a9ba87e2c3d757ca019fa576793e8758174d3868aecb88d6fc8eb",
+ "sha256:bf8fc66012ca857d62f6a347007e166ed59c0bc150cefa49f28376ebe7d992a2",
+ "sha256:f5d0bf9b252f30a31664b6f64432b4730bb7038339bd18b1fafe129cfc2be9be"
+ ],
+ "index": "pypi",
+ "version": "==41.0.0"
},
"deprecated": {
"hashes": [
- "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d",
- "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"
+ "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c",
+ "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
- "version": "==1.2.13"
+ "version": "==1.2.14"
},
"dnspython": {
"hashes": [
"sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9",
"sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"
],
- "markers": "python_version >= '3.7' and python_version < '4.0'",
+ "markers": "python_version >= '3.7' and python_version < '4'",
"version": "==2.3.0"
},
"email-validator": {
@@ -491,7 +491,7 @@
},
"google-api-core": {
"extras": [
- "grpc"
+
],
"hashes": [
"sha256:4b9bb5d5a380a0befa0573b302651b8a9a89262c1730e37bf423cec511804c22",
@@ -502,11 +502,11 @@
},
"google-auth": {
"hashes": [
- "sha256:ce311e2bc58b130fddf316df57c9b3943c2a7b4f6ec31de9663a9333e4064efc",
- "sha256:f586b274d3eb7bd932ea424b1c702a30e0393a2e2bc4ca3eae8263ffd8be229f"
+ "sha256:a9cfa88b3e16196845e64a3658eb953992129d13ac7337b064c6546f77c17183",
+ "sha256:ea165e014c7cbd496558796b627c271aa8c18b4cba79dc1cc962b24c5efdfb85"
],
- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
- "version": "==2.17.3"
+ "markers": "python_version >= '3.6'",
+ "version": "==2.19.1"
},
"google-cloud-core": {
"hashes": [
@@ -844,10 +844,10 @@
},
"jwcrypto": {
"hashes": [
- "sha256:80a35e9ed1b3b2c43ce03d92c5d48e6d0b6647e2aa2618e4963448923d78a37b"
+ "sha256:2c1dc51cf8e38ddf324795dfe9426dee9dd46caf47f535ccbc18781fba810b8d"
],
"markers": "python_version >= '3.6'",
- "version": "==1.4.2"
+ "version": "==1.5.0"
},
"markupsafe": {
"hashes": [
@@ -956,22 +956,22 @@
},
"protobuf": {
"hashes": [
- "sha256:13233ee2b9d3bd9a5f216c1fa2c321cd564b93d8f2e4f521a85b585447747997",
- "sha256:23452f2fdea754a8251d0fc88c0317735ae47217e0d27bf330a30eec2848811a",
- "sha256:52f0a78141078077cfe15fe333ac3e3a077420b9a3f5d1bf9b5fe9d286b4d881",
- "sha256:70659847ee57a5262a65954538088a1d72dfc3e9882695cab9f0c54ffe71663b",
- "sha256:7760730063329d42a9d4c4573b804289b738d4931e363ffbe684716b796bde51",
- "sha256:7cf56e31907c532e460bb62010a513408e6cdf5b03fb2611e4b67ed398ad046d",
- "sha256:8b54f56d13ae4a3ec140076c9d937221f887c8f64954673d46f63751209e839a",
- "sha256:d14fc1a41d1a1909998e8aff7e80d2a7ae14772c4a70e4bf7db8a36690b54425",
- "sha256:d4b66266965598ff4c291416be429cef7989d8fae88b55b62095a2331511b3fa",
- "sha256:e0e630d8e6a79f48c557cd1835865b593d0547dce221c66ed1b827de59c66c97",
- "sha256:ecae944c6c2ce50dda6bf76ef5496196aeb1b85acb95df5843cd812615ec4b61",
- "sha256:f08aa300b67f1c012100d8eb62d47129e53d1150f4469fd78a29fa3cb68c66f2",
- "sha256:f2f4710543abec186aee332d6852ef5ae7ce2e9e807a3da570f36de5a732d88e"
+ "sha256:09310bce43353b46d73ba7e3bca78273b9bc50349509b9698e64d288c6372c2a",
+ "sha256:20874e7ca4436f683b64ebdbee2129a5a2c301579a67d1a7dda2cdf62fb7f5f7",
+ "sha256:25e3370eda26469b58b602e29dff069cfaae8eaa0ef4550039cc5ef8dc004511",
+ "sha256:281342ea5eb631c86697e1e048cb7e73b8a4e85f3299a128c116f05f5c668f8f",
+ "sha256:384dd44cb4c43f2ccddd3645389a23ae61aeb8cfa15ca3a0f60e7c3ea09b28b3",
+ "sha256:54a533b971288af3b9926e53850c7eb186886c0c84e61daa8444385a4720297f",
+ "sha256:6c081863c379bb1741be8f8193e893511312b1d7329b4a75445d1ea9955be69e",
+ "sha256:86df87016d290143c7ce3be3ad52d055714ebaebb57cc659c387e76cfacd81aa",
+ "sha256:8da6070310d634c99c0db7df48f10da495cc283fd9e9234877f0cd182d43ab7f",
+ "sha256:b2cfab63a230b39ae603834718db74ac11e52bccaaf19bf20f5cce1a84cf76df",
+ "sha256:c52cfcbfba8eb791255edd675c1fe6056f723bf832fa67f0442218f8817c076e",
+ "sha256:ce744938406de1e64b91410f473736e815f28c3b71201302612a68bf01517fea",
+ "sha256:efabbbbac1ab519a514579ba9ec52f006c28ae19d97915951f69fa70da2c9e91"
],
"markers": "python_version >= '3.7'",
- "version": "==4.22.3"
+ "version": "==4.23.2"
},
"pyasn1": {
"hashes": [
@@ -1132,16 +1132,16 @@
"sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7",
"sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"
],
- "markers": "python_version >= '3.6'",
+ "markers": "python_version >= '3.6' and python_version < '4'",
"version": "==4.9"
},
"s3transfer": {
"hashes": [
- "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
- "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
+ "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346",
+ "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"
],
"markers": "python_version >= '3.7'",
- "version": "==0.6.0"
+ "version": "==0.6.1"
},
"sdc-cryptography": {
"hashes": [
@@ -1153,11 +1153,11 @@
},
"setuptools": {
"hashes": [
- "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b",
- "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"
+ "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f",
+ "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"
],
"markers": "python_version >= '3.7'",
- "version": "==67.7.2"
+ "version": "==67.8.0"
},
"simplejson": {
"hashes": [
@@ -1276,11 +1276,11 @@
},
"urllib3": {
"hashes": [
- "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305",
- "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"
+ "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
+ "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
- "version": "==1.26.15"
+ "version": "==1.26.16"
},
"uwsgi": {
"hashes": [
@@ -1291,11 +1291,11 @@
},
"werkzeug": {
"hashes": [
- "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a",
- "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"
+ "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76",
+ "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f"
],
"markers": "python_version >= '3.8'",
- "version": "==2.3.3"
+ "version": "==2.3.4"
},
"wrapt": {
"hashes": [
@@ -1433,11 +1433,11 @@
"develop": {
"astroid": {
"hashes": [
- "sha256:a1b8543ef9d36ea777194bc9b17f5f8678d2c56ee6a45b2c2f17eec96f242347",
- "sha256:c81e1c7fbac615037744d067a9bb5f9aeb655edf59b63ee8b59585475d6f80d8"
+ "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324",
+ "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f"
],
"markers": "python_full_version >= '3.7.2'",
- "version": "==2.15.4"
+ "version": "==2.15.5"
},
"async-timeout": {
"hashes": [
@@ -1494,6 +1494,14 @@
"index": "pypi",
"version": "==23.3.0"
},
+ "blinker": {
+ "hashes": [
+ "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213",
+ "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"
+ ],
+ "index": "pypi",
+ "version": "==1.6.2"
+ },
"boto3": {
"hashes": [
"sha256:6648aff15d19927cd26db47eb56362ccd313a1ddbd7aaa3235ef05d05d398252",
@@ -1504,19 +1512,19 @@
},
"botocore": {
"hashes": [
- "sha256:3005a7ffee083315e69938acdf1bfeaf9e21fe1fe1643d6573ee817721f4ffcd",
- "sha256:ac87b63e9aa4231cd28941945024a0c4470c184c60334ebe5e1cae3544c785ed"
+ "sha256:77f7793cb36074eb84d606a23ad6e1d57c20f7a2eeab7d9136d3e63c584e0504",
+ "sha256:ac57003292f18206ee942eafc381ecd9a3420a3844d6b7e1c1b0f4b88b28263b"
],
"markers": "python_version >= '3.7'",
- "version": "==1.29.125"
+ "version": "==1.29.146"
},
"certifi": {
"hashes": [
- "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
- "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
+ "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
+ "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
],
"markers": "python_version >= '3.6'",
- "version": "==2022.12.7"
+ "version": "==2023.5.7"
},
"cffi": {
"hashes": [
@@ -1665,7 +1673,7 @@
"sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df",
"sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"
],
- "markers": "python_full_version >= '3.7.0'",
+ "markers": "python_version >= '3.7'",
"version": "==3.1.0"
},
"click": {
@@ -1681,85 +1689,94 @@
"toml"
],
"hashes": [
- "sha256:0342a28617e63ad15d96dca0f7ae9479a37b7d8a295f749c14f3436ea59fdcb3",
- "sha256:066b44897c493e0dcbc9e6a6d9f8bbb6607ef82367cf6810d387c09f0cd4fe9a",
- "sha256:10b15394c13544fce02382360cab54e51a9e0fd1bd61ae9ce012c0d1e103c813",
- "sha256:12580845917b1e59f8a1c2ffa6af6d0908cb39220f3019e36c110c943dc875b0",
- "sha256:156192e5fd3dbbcb11cd777cc469cf010a294f4c736a2b2c891c77618cb1379a",
- "sha256:1637253b11a18f453e34013c665d8bf15904c9e3c44fbda34c643fbdc9d452cd",
- "sha256:292300f76440651529b8ceec283a9370532f4ecba9ad67d120617021bb5ef139",
- "sha256:30dcaf05adfa69c2a7b9f7dfd9f60bc8e36b282d7ed25c308ef9e114de7fc23b",
- "sha256:338aa9d9883aaaad53695cb14ccdeb36d4060485bb9388446330bef9c361c252",
- "sha256:373ea34dca98f2fdb3e5cb33d83b6d801007a8074f992b80311fc589d3e6b790",
- "sha256:38c0a497a000d50491055805313ed83ddba069353d102ece8aef5d11b5faf045",
- "sha256:40cc0f91c6cde033da493227797be2826cbf8f388eaa36a0271a97a332bfd7ce",
- "sha256:4436cc9ba5414c2c998eaedee5343f49c02ca93b21769c5fdfa4f9d799e84200",
- "sha256:509ecd8334c380000d259dc66feb191dd0a93b21f2453faa75f7f9cdcefc0718",
- "sha256:5c587f52c81211d4530fa6857884d37f514bcf9453bdeee0ff93eaaf906a5c1b",
- "sha256:5f3671662dc4b422b15776cdca89c041a6349b4864a43aa2350b6b0b03bbcc7f",
- "sha256:6599bf92f33ab041e36e06d25890afbdf12078aacfe1f1d08c713906e49a3fe5",
- "sha256:6e8a95f243d01ba572341c52f89f3acb98a3b6d1d5d830efba86033dd3687ade",
- "sha256:706ec567267c96717ab9363904d846ec009a48d5f832140b6ad08aad3791b1f5",
- "sha256:780551e47d62095e088f251f5db428473c26db7829884323e56d9c0c3118791a",
- "sha256:7ff8f3fb38233035028dbc93715551d81eadc110199e14bbbfa01c5c4a43f8d8",
- "sha256:828189fcdda99aae0d6bf718ea766b2e715eabc1868670a0a07bf8404bf58c33",
- "sha256:857abe2fa6a4973f8663e039ead8d22215d31db613ace76e4a98f52ec919068e",
- "sha256:883123d0bbe1c136f76b56276074b0c79b5817dd4238097ffa64ac67257f4b6c",
- "sha256:8877d9b437b35a85c18e3c6499b23674684bf690f5d96c1006a1ef61f9fdf0f3",
- "sha256:8e575a59315a91ccd00c7757127f6b2488c2f914096077c745c2f1ba5b8c0969",
- "sha256:97072cc90f1009386c8a5b7de9d4fc1a9f91ba5ef2146c55c1f005e7b5c5e068",
- "sha256:9a22cbb5ede6fade0482111fa7f01115ff04039795d7092ed0db43522431b4f2",
- "sha256:a063aad9f7b4c9f9da7b2550eae0a582ffc7623dca1c925e50c3fbde7a579771",
- "sha256:a08c7401d0b24e8c2982f4e307124b671c6736d40d1c39e09d7a8687bddf83ed",
- "sha256:a0b273fe6dc655b110e8dc89b8ec7f1a778d78c9fd9b4bda7c384c8906072212",
- "sha256:a2b3b05e22a77bb0ae1a3125126a4e08535961c946b62f30985535ed40e26614",
- "sha256:a66e055254a26c82aead7ff420d9fa8dc2da10c82679ea850d8feebf11074d88",
- "sha256:aa387bd7489f3e1787ff82068b295bcaafbf6f79c3dad3cbc82ef88ce3f48ad3",
- "sha256:ae453f655640157d76209f42c62c64c4d4f2c7f97256d3567e3b439bd5c9b06c",
- "sha256:b5016e331b75310610c2cf955d9f58a9749943ed5f7b8cfc0bb89c6134ab0a84",
- "sha256:b9a4ee55174b04f6af539218f9f8083140f61a46eabcaa4234f3c2a452c4ed11",
- "sha256:bd3b4b8175c1db502adf209d06136c000df4d245105c8839e9d0be71c94aefe1",
- "sha256:bebea5f5ed41f618797ce3ffb4606c64a5de92e9c3f26d26c2e0aae292f015c1",
- "sha256:c10fbc8a64aa0f3ed136b0b086b6b577bc64d67d5581acd7cc129af52654384e",
- "sha256:c2c41c1b1866b670573657d584de413df701f482574bad7e28214a2362cb1fd1",
- "sha256:cf97ed82ca986e5c637ea286ba2793c85325b30f869bf64d3009ccc1a31ae3fd",
- "sha256:d1f25ee9de21a39b3a8516f2c5feb8de248f17da7eead089c2e04aa097936b47",
- "sha256:d2fbc2a127e857d2f8898aaabcc34c37771bf78a4d5e17d3e1f5c30cd0cbc62a",
- "sha256:dc945064a8783b86fcce9a0a705abd7db2117d95e340df8a4333f00be5efb64c",
- "sha256:ddc5a54edb653e9e215f75de377354e2455376f416c4378e1d43b08ec50acc31",
- "sha256:e8834e5f17d89e05697c3c043d3e58a8b19682bf365048837383abfe39adaed5",
- "sha256:ef9659d1cda9ce9ac9585c045aaa1e59223b143f2407db0eaee0b61a4f266fb6",
- "sha256:f6f5cab2d7f0c12f8187a376cc6582c477d2df91d63f75341307fcdcb5d60303",
- "sha256:f81c9b4bd8aa747d417407a7f6f0b1469a43b36a85748145e144ac4e8d303cb5",
- "sha256:f99ef080288f09ffc687423b8d60978cf3a465d3f404a18d1a05474bd8575a47"
+ "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f",
+ "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2",
+ "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a",
+ "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a",
+ "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01",
+ "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6",
+ "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7",
+ "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f",
+ "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02",
+ "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c",
+ "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063",
+ "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a",
+ "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5",
+ "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959",
+ "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97",
+ "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6",
+ "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f",
+ "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9",
+ "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5",
+ "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f",
+ "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562",
+ "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe",
+ "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9",
+ "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f",
+ "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb",
+ "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb",
+ "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1",
+ "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb",
+ "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250",
+ "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e",
+ "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511",
+ "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5",
+ "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59",
+ "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2",
+ "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d",
+ "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3",
+ "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4",
+ "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de",
+ "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9",
+ "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833",
+ "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0",
+ "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9",
+ "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d",
+ "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050",
+ "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d",
+ "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6",
+ "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353",
+ "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb",
+ "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e",
+ "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8",
+ "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495",
+ "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2",
+ "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd",
+ "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27",
+ "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1",
+ "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818",
+ "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4",
+ "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e",
+ "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850",
+ "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"
],
"markers": "python_version >= '3.7'",
- "version": "==7.2.5"
+ "version": "==7.2.7"
},
"cryptography": {
"hashes": [
- "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440",
- "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288",
- "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b",
- "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958",
- "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b",
- "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d",
- "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a",
- "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404",
- "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b",
- "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e",
- "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2",
- "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c",
- "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b",
- "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9",
- "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b",
- "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636",
- "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99",
- "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e",
- "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"
- ],
- "markers": "python_version >= '3.6'",
- "version": "==40.0.2"
+ "sha256:0ddaee209d1cf1f180f1efa338a68c4621154de0afaef92b89486f5f96047c55",
+ "sha256:14754bcdae909d66ff24b7b5f166d69340ccc6cb15731670435efd5719294895",
+ "sha256:344c6de9f8bda3c425b3a41b319522ba3208551b70c2ae00099c205f0d9fd3be",
+ "sha256:34d405ea69a8b34566ba3dfb0521379b210ea5d560fafedf9f800a9a94a41928",
+ "sha256:3680248309d340fda9611498a5319b0193a8dbdb73586a1acf8109d06f25b92d",
+ "sha256:3c5ef25d060c80d6d9f7f9892e1d41bb1c79b78ce74805b8cb4aa373cb7d5ec8",
+ "sha256:4ab14d567f7bbe7f1cdff1c53d5324ed4d3fc8bd17c481b395db224fb405c237",
+ "sha256:5c1f7293c31ebc72163a9a0df246f890d65f66b4a40d9ec80081969ba8c78cc9",
+ "sha256:6b71f64beeea341c9b4f963b48ee3b62d62d57ba93eb120e1196b31dc1025e78",
+ "sha256:7d92f0248d38faa411d17f4107fc0bce0c42cae0b0ba5415505df72d751bf62d",
+ "sha256:8362565b3835ceacf4dc8f3b56471a2289cf51ac80946f9087e66dc283a810e0",
+ "sha256:84a165379cb9d411d58ed739e4af3396e544eac190805a54ba2e0322feb55c46",
+ "sha256:88ff107f211ea696455ea8d911389f6d2b276aabf3231bf72c8853d22db755c5",
+ "sha256:9f65e842cb02550fac96536edb1d17f24c0a338fd84eaf582be25926e993dde4",
+ "sha256:a4fc68d1c5b951cfb72dfd54702afdbbf0fb7acdc9b7dc4301bbf2225a27714d",
+ "sha256:b7f2f5c525a642cecad24ee8670443ba27ac1fab81bba4cc24c7b6b41f2d0c75",
+ "sha256:b846d59a8d5a9ba87e2c3d757ca019fa576793e8758174d3868aecb88d6fc8eb",
+ "sha256:bf8fc66012ca857d62f6a347007e166ed59c0bc150cefa49f28376ebe7d992a2",
+ "sha256:f5d0bf9b252f30a31664b6f64432b4730bb7038339bd18b1fafe129cfc2be9be"
+ ],
+ "index": "pypi",
+ "version": "==41.0.0"
},
"dill": {
"hashes": [
@@ -2107,11 +2124,11 @@
},
"platformdirs": {
"hashes": [
- "sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4",
- "sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335"
+ "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f",
+ "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"
],
"markers": "python_version >= '3.7'",
- "version": "==3.5.0"
+ "version": "==3.5.1"
},
"pluggy": {
"hashes": [
@@ -2328,11 +2345,11 @@
},
"s3transfer": {
"hashes": [
- "sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
- "sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
+ "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346",
+ "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"
],
"markers": "python_version >= '3.7'",
- "version": "==0.6.0"
+ "version": "==0.6.1"
},
"six": {
"hashes": [
@@ -2383,10 +2400,10 @@
},
"types-pyopenssl": {
"hashes": [
- "sha256:20b80971b86240e8432a1832bd8124cea49c3088c7bfc77dfd23be27ffe4a517",
- "sha256:b050641aeff6dfebf231ad719bdac12d53b8ee818d4afb67b886333484629957"
+ "sha256:43e307e8dfb3a7a8208a19874ca060305f460c529d4eaca8a2669ea89499f244",
+ "sha256:ba803a99440b0c2e9ab4e197084aeefc55bdfe8a580d367b2aa4210810a21240"
],
- "version": "==23.1.0.2"
+ "version": "==23.2.0.0"
},
"types-python-dateutil": {
"hashes": [
@@ -2430,34 +2447,34 @@
},
"types-urllib3": {
"hashes": [
- "sha256:3ba3d3a8ee46e0d5512c6bd0594da4f10b2584b47a470f8422044a2ab462f1df",
- "sha256:a1557355ce8d350a555d142589f3001903757d2d36c18a66f588d9659bbc917d"
+ "sha256:3300538c9dc11dad32eae4827ac313f5d986b8b21494801f1bf97a1ac6c03ae5",
+ "sha256:5dbd1d2bef14efee43f5318b5d36d805a489f6600252bb53626d4bfafd95e27c"
],
- "version": "==1.26.25.12"
+ "version": "==1.26.25.13"
},
"typing-extensions": {
"hashes": [
- "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb",
- "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"
+ "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26",
+ "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"
],
"markers": "python_version >= '3.7'",
- "version": "==4.5.0"
+ "version": "==4.6.3"
},
"urllib3": {
"hashes": [
- "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305",
- "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"
+ "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
+ "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
- "version": "==1.26.15"
+ "version": "==1.26.16"
},
"werkzeug": {
"hashes": [
- "sha256:4866679a0722de00796a74086238bb3b98d90f423f05de039abb09315487254a",
- "sha256:a987caf1092edc7523edb139edb20c70571c4a8d5eed02e0b547b4739174d091"
+ "sha256:1d5a58e0377d1fe39d061a5de4469e414e78ccb1e1e59c0f5ad6fa1c36c52b76",
+ "sha256:48e5e61472fee0ddee27ebad085614ebedb7af41e88f687aaf881afb723a162f"
],
"markers": "python_version >= '3.8'",
- "version": "==2.3.3"
+ "version": "==2.3.4"
},
"wrapt": {
"hashes": [
From d2dc26a15d8e8ec3a4310f46e85f582d070e36e5 Mon Sep 17 00:00:00 2001
From: petechd <53475968+petechd@users.noreply.github.com>
Date: Mon, 26 Jun 2023 13:37:58 +0100
Subject: [PATCH 20/23] Fix mass-metric-ton units handling (#1146)
---
app/jinja_filters.py | 24 ++++++++++++------------
tests/app/test_jinja_filters.py | 5 +++++
2 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/app/jinja_filters.py b/app/jinja_filters.py
index 834a036502..5f711d60f0 100644
--- a/app/jinja_filters.py
+++ b/app/jinja_filters.py
@@ -75,12 +75,17 @@ def format_unit(
value: int | float | Decimal,
length: UnitLengthType = "short",
) -> str:
+ # mass-metric-ton no longer supported for en_GB and related locales, but still present in business schema and allowed in validator,
+ # until removed from schema we substitute mass-tonne for mass-metric-ton before format unit
+ measurement_unit = "mass-tonne" if unit == "mass-metric-ton" else unit
+
formatted_unit: str = units.format_unit(
value=value,
- measurement_unit=unit,
+ measurement_unit=measurement_unit,
length=length,
locale=flask_babel.get_locale(),
)
+
return formatted_unit
@@ -93,20 +98,15 @@ def format_unit_input_label(unit: str, unit_length: UnitLengthType = "short") ->
:param (str) unit_length length of unit text, can be one of short/long/narrow
"""
unit_label: str
+
if unit_length == "long":
- unit_label = units.format_unit(
- value=2,
- measurement_unit=unit,
- length=unit_length,
- locale=flask_babel.get_locale(),
- ).replace("2 ", "")
+ unit_label = format_unit(value=2, unit=unit, length=unit_length).replace(
+ "2 ", ""
+ )
else:
# Type ignore: We pass an empty string as the value so that we just return the unit label
- unit_label = units.format_unit(
- value="", # type: ignore
- measurement_unit=unit,
- length=unit_length,
- locale=flask_babel.get_locale(),
+ unit_label = format_unit(
+ value="", unit=unit, length=unit_length # type: ignore
).strip()
return unit_label
diff --git a/tests/app/test_jinja_filters.py b/tests/app/test_jinja_filters.py
index d76c7c55fa..18473b779a 100644
--- a/tests/app/test_jinja_filters.py
+++ b/tests/app/test_jinja_filters.py
@@ -153,6 +153,9 @@ def test_format_percentage(percentage, formatted_percentage):
("duration-year", 100, "short", "100 bl", "cy"),
("duration-hour", 100, "long", "100 awr", "cy"),
("duration-year", 100, "long", "100 mlynedd", "cy"),
+ ("mass-metric-ton", 100, "long", "100 tonnes", "en_GB"),
+ ("mass-metric-ton", 1, "long", "1 tonne", "en_GB"),
+ ("mass-metric-ton", 100, "short", "100 t", "en_GB"),
),
)
def test_format_unit(unit, value, length, formatted_unit, language, mocker):
@@ -204,6 +207,8 @@ def test_format_unit(unit, value, length, formatted_unit, language, mocker):
("duration-hour", "long", "awr", "cy"),
("duration-year", "short", "bl", "cy"),
("duration-year", "long", "flynedd", "cy"),
+ ("mass-metric-ton", "long", "tonnes", "en_GB"),
+ ("mass-metric-ton", "short", "t", "en_GB"),
),
)
def test_format_unit_input_label(unit, length, formatted_unit, language, mocker):
From deb64a7ad74100df961c687ff8359954950070a6 Mon Sep 17 00:00:00 2001
From: petechd <53475968+petechd@users.noreply.github.com>
Date: Fri, 30 Jun 2023 13:10:35 +0100
Subject: [PATCH 21/23] Fix unit label hover title for metric ton (#1150)
---
app/setup.py | 7 ++++++-
schemas/test/en/test_unit_patterns.json | 19 +++++++++++++++++++
tests/functional/spec/features/units.spec.js | 19 ++++++++++++++++---
3 files changed, 41 insertions(+), 4 deletions(-)
diff --git a/app/setup.py b/app/setup.py
index a4f9a6d64f..81f9df690c 100644
--- a/app/setup.py
+++ b/app/setup.py
@@ -36,6 +36,7 @@
from app.routes.schema import schema_blueprint
from app.routes.session import session_blueprint
from app.secrets import SecretStore, validate_required_secrets
+from app.settings import DEFAULT_LOCALE
from app.storage import Datastore, Dynamodb, Redis
from app.submitter import (
GCSFeedbackSubmitter,
@@ -479,7 +480,11 @@ def get_minimized_asset(filename):
def get_locale():
- return cookie_session.get("language_code")
+ return (
+ DEFAULT_LOCALE
+ if cookie_session.get("language_code") == "en"
+ else cookie_session.get("language_code")
+ )
def get_timezone():
diff --git a/schemas/test/en/test_unit_patterns.json b/schemas/test/en/test_unit_patterns.json
index d54a8bca90..f3d6ed684f 100644
--- a/schemas/test/en/test_unit_patterns.json
+++ b/schemas/test/en/test_unit_patterns.json
@@ -243,6 +243,25 @@
"title": "Volume Units",
"type": "General"
}
+ },
+ {
+ "type": "Question",
+ "id": "set-weight-units-block",
+ "question": {
+ "answers": [
+ {
+ "id": "mass-metric-ton",
+ "label": "Mass tonnes",
+ "mandatory": false,
+ "type": "Unit",
+ "unit": "mass-metric-ton",
+ "unit_length": "short"
+ }
+ ],
+ "id": "set-weight-unit-questions",
+ "title": "Weight Units",
+ "type": "General"
+ }
}
],
"id": "test"
diff --git a/tests/functional/spec/features/units.spec.js b/tests/functional/spec/features/units.spec.js
index 6c3b60e5d6..0873bbf8fb 100644
--- a/tests/functional/spec/features/units.spec.js
+++ b/tests/functional/spec/features/units.spec.js
@@ -2,6 +2,7 @@ import SetLengthUnitsBlockPage from "../../generated_pages/unit_patterns/set-len
import SetDurationUnitsBlockPage from "../../generated_pages/unit_patterns/set-duration-units-block.page.js";
import SetAreaUnitsBlockPage from "../../generated_pages/unit_patterns/set-area-units-block.page.js";
import SetVolumeUnitsBlockPage from "../../generated_pages/unit_patterns/set-volume-units-block.page.js";
+import SetWeightUnitsBlockPage from "../../generated_pages/unit_patterns/set-weight-units-block.page.js";
import SubmitPage from "../../generated_pages/unit_patterns/submit.page.js";
describe("Units", () => {
@@ -15,6 +16,7 @@ describe("Units", () => {
await $(SetDurationUnitsBlockPage.submit()).click();
await $(SetAreaUnitsBlockPage.submit()).click();
await $(SetVolumeUnitsBlockPage.submit()).click();
+ await $(SetWeightUnitsBlockPage.submit()).click();
await expect(await $(SubmitPage.durationHour()).getText()).to.equal("6 hours");
await expect(await $(SubmitPage.durationYear()).getText()).to.equal("20 years");
});
@@ -31,15 +33,26 @@ describe("Units", () => {
await $(SetDurationUnitsBlockPage.submit()).click();
await $(SetAreaUnitsBlockPage.submit()).click();
await $(SetVolumeUnitsBlockPage.submit()).click();
+ await $(SetWeightUnitsBlockPage.submit()).click();
await expect(await $(SubmitPage.durationHour()).getText()).to.equal("6 awr");
await expect(await $(SubmitPage.durationYear()).getText()).to.equal("20 mlynedd");
});
it("Given we open a questionnaire with unit labels, when the label is highlighted by the tooltip, then the long unit label should be displayed.", async () => {
await browser.openQuestionnaire("test_unit_patterns.json", { language: "en" });
- await expect(await $(SetLengthUnitsBlockPage.centimetresUnit()).getAttribute("title")).to.equal("centimeters");
- await expect(await $(SetLengthUnitsBlockPage.metresUnit()).getAttribute("title")).to.equal("meters");
- await expect(await $(SetLengthUnitsBlockPage.kilometresUnit()).getAttribute("title")).to.equal("kilometers");
+ await expect(await $(SetLengthUnitsBlockPage.centimetresUnit()).getAttribute("title")).to.equal("centimetres");
+ await expect(await $(SetLengthUnitsBlockPage.metresUnit()).getAttribute("title")).to.equal("metres");
+ await expect(await $(SetLengthUnitsBlockPage.kilometresUnit()).getAttribute("title")).to.equal("kilometres");
await expect(await $(SetLengthUnitsBlockPage.milesUnit()).getAttribute("title")).to.equal("miles");
});
+
+ it("Given we open a questionnaire with unit labels, when the weight unit label is highlighted by the tooltip, then the correct unit label should be displayed.", async () => {
+ await browser.openQuestionnaire("test_unit_patterns.json", { language: "en" });
+ await $(SetLengthUnitsBlockPage.submit()).click();
+ await $(SetDurationUnitsBlockPage.submit()).click();
+ await $(SetAreaUnitsBlockPage.submit()).click();
+ await $(SetVolumeUnitsBlockPage.submit()).click();
+ await expect(await $("body").getText()).to.have.string("tonnes");
+ await expect(await $("body").getText()).to.not.have.string("metric tons");
+ });
});
From f11fa2cb5d1bcd0766f41846a15b2689e856a416 Mon Sep 17 00:00:00 2001
From: petechd <53475968+petechd@users.noreply.github.com>
Date: Mon, 3 Jul 2023 15:42:27 +0100
Subject: [PATCH 22/23] Fix list collector placeholders not resolving when
preview enabled (#1151)
---
app/views/contexts/context.py | 3 ++-
app/views/contexts/preview_context.py | 1 +
app/views/contexts/section_preview_context.py | 1 +
schemas/test/en/test_list_collector_list_summary.json | 5 +++++
tests/functional/spec/list_collector.spec.js | 2 ++
5 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/app/views/contexts/context.py b/app/views/contexts/context.py
index 9495e08c45..a0033abd94 100644
--- a/app/views/contexts/context.py
+++ b/app/views/contexts/context.py
@@ -20,6 +20,7 @@ def __init__(
progress_store: ProgressStore,
metadata: MetadataProxy | None,
response_metadata: MutableMapping,
+ placeholder_preview_mode: bool = False,
) -> None:
self._language = language
self._schema = schema
@@ -28,7 +29,7 @@ def __init__(
self._progress_store = progress_store
self._metadata = metadata
self._response_metadata = response_metadata
- self._placeholder_preview_mode = self._schema.preview_enabled
+ self._placeholder_preview_mode = placeholder_preview_mode
self._router = Router(
schema=self._schema,
diff --git a/app/views/contexts/preview_context.py b/app/views/contexts/preview_context.py
index 7e173250c6..057cbe73d6 100644
--- a/app/views/contexts/preview_context.py
+++ b/app/views/contexts/preview_context.py
@@ -35,6 +35,7 @@ def __init__(
progress_store,
metadata,
response_metadata,
+ placeholder_preview_mode=True,
)
def __call__(self) -> dict[str, Union[str, list, bool]]:
diff --git a/app/views/contexts/section_preview_context.py b/app/views/contexts/section_preview_context.py
index 325897752c..5a139ff376 100644
--- a/app/views/contexts/section_preview_context.py
+++ b/app/views/contexts/section_preview_context.py
@@ -28,6 +28,7 @@ def __init__(
progress_store,
metadata,
response_metadata,
+ placeholder_preview_mode=True,
)
self._section_id = section_id
diff --git a/schemas/test/en/test_list_collector_list_summary.json b/schemas/test/en/test_list_collector_list_summary.json
index a7637c7bf7..82aae3b14d 100644
--- a/schemas/test/en/test_list_collector_list_summary.json
+++ b/schemas/test/en/test_list_collector_list_summary.json
@@ -5,6 +5,7 @@
"data_version": "0.0.3",
"survey_id": "0",
"title": "Test ListCollector",
+ "preview_questions": true,
"theme": "default",
"description": "A questionnaire to test ListCollector",
"metadata": [
@@ -111,6 +112,10 @@
"id": "group",
"title": "Questions",
"blocks": [
+ {
+ "id": "introduction",
+ "type": "Introduction"
+ },
{
"id": "primary-person-list-collector",
"type": "PrimaryPersonListCollector",
diff --git a/tests/functional/spec/list_collector.spec.js b/tests/functional/spec/list_collector.spec.js
index 8894a68f82..a3ea2ab759 100644
--- a/tests/functional/spec/list_collector.spec.js
+++ b/tests/functional/spec/list_collector.spec.js
@@ -19,6 +19,7 @@ import VisitorListCollectorPage from "../generated_pages/list_collector_list_sum
import VisitorListCollectorAddPage from "../generated_pages/list_collector_list_summary/visitor-list-collector-add.page.js";
import PeopleListSectionSummaryPage from "../generated_pages/list_collector_list_summary/section-summary.page.js";
import { SubmitPage } from "../base_pages/submit.page.js";
+import IntroductionPage from "../generated_pages/list_collector_list_summary/introduction.page.js";
describe("List Collector", () => {
describe("Given a normal journey through the list collector without variants", () => {
@@ -172,6 +173,7 @@ describe("List Collector", () => {
describe("Given I start a list collector survey and complete to Section Summary", () => {
beforeEach(async () => {
await browser.openQuestionnaire("test_list_collector_list_summary.json");
+ await $(IntroductionPage.submit()).click();
await $(PrimaryPersonListCollectorPage.yes()).click();
await $(PrimaryPersonListCollectorPage.submit()).click();
await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus");
From fa46ce75c790b66d331ba66f47b4b6ce659ec5ef Mon Sep 17 00:00:00 2001
From: petechd <53475968+petechd@users.noreply.github.com>
Date: Wed, 5 Jul 2023 09:13:13 +0100
Subject: [PATCH 23/23] Schemas v3.63.0 (#1155)
---
.schemas-version | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.schemas-version b/.schemas-version
index 5cd3892448..7b705cd7be 100644
--- a/.schemas-version
+++ b/.schemas-version
@@ -1 +1 @@
-v3.62.0
+v3.63.0