From 57e6d25bb38888e732daf1a1a1d29e579ac05fbb Mon Sep 17 00:00:00 2001 From: Rudi Giesler Date: Tue, 3 Dec 2024 13:34:21 +0200 Subject: [PATCH 1/7] Add GoToForm button and list item types --- .../0087_alter_contentpage_whatsapp_body.py | 313 ++++++++++++++++++ home/models.py | 19 +- 2 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 home/migrations/0087_alter_contentpage_whatsapp_body.py diff --git a/home/migrations/0087_alter_contentpage_whatsapp_body.py b/home/migrations/0087_alter_contentpage_whatsapp_body.py new file mode 100644 index 00000000..fc87db33 --- /dev/null +++ b/home/migrations/0087_alter_contentpage_whatsapp_body.py @@ -0,0 +1,313 @@ +# Generated by Django 4.2.11 on 2024-12-03 11:33 + +import django.core.validators +from django.db import migrations +import home.models +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +import wagtail.images.blocks +import wagtail.snippets.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ("home", "0086_orderedcontentset_set_locale_and_add_slug"), + ] + + operations = [ + migrations.AlterField( + model_name="contentpage", + name="whatsapp_body", + field=wagtail.fields.StreamField( + [ + ( + "Whatsapp_Message", + wagtail.blocks.StructBlock( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock( + required=False + ), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + icon="document", required=False + ), + ), + ( + "media", + home.models.MediaBlock( + icon="media", required=False + ), + ), + ( + "message", + wagtail.blocks.TextBlock( + help_text="each text message cannot exceed 4096 characters, messages with media cannot exceed 1024 characters.", + validators=( + django.core.validators.MaxLengthValidator( + 4096 + ), + ), + ), + ), + ( + "example_values", + wagtail.blocks.ListBlock( + wagtail.blocks.CharBlock(label="Example Value"), + default=[], + help_text="Please add example values for all variables used in a WhatsApp template", + label="Variable Example Values", + ), + ), + ( + "variation_messages", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "variation_restrictions", + wagtail.blocks.StreamBlock( + [ + ( + "gender", + wagtail.blocks.ChoiceBlock( + choices=home.models.get_gender_choices + ), + ), + ( + "age", + wagtail.blocks.ChoiceBlock( + choices=home.models.get_age_choices + ), + ), + ( + "relationship", + wagtail.blocks.ChoiceBlock( + choices=home.models.get_relationship_choices + ), + ), + ], + help_text="Restrict this variation to users with this profile value. Valid values must be added to the Site Settings", + max_num=1, + min_num=1, + required=False, + use_json_field=True, + ), + ), + ( + "message", + wagtail.blocks.TextBlock( + help_text="each message cannot exceed 4096 characters.", + validators=( + django.core.validators.MaxLengthValidator( + 4096 + ), + ), + ), + ), + ] + ), + default=[], + ), + ), + ( + "next_prompt", + wagtail.blocks.CharBlock( + help_text="prompt text for next message", + required=False, + validators=( + django.core.validators.MaxLengthValidator( + 20 + ), + ), + ), + ), + ( + "buttons", + wagtail.blocks.StreamBlock( + [ + ( + "next_message", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Text for the button, up to 20 characters.", + validators=( + django.core.validators.MaxLengthValidator( + 20 + ), + ), + ), + ) + ] + ), + ), + ( + "go_to_page", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Text for the button, up to 20 characters.", + validators=( + django.core.validators.MaxLengthValidator( + 20 + ), + ), + ), + ), + ( + "page", + wagtail.blocks.PageChooserBlock( + help_text="Page the button should go to" + ), + ), + ] + ), + ), + ( + "go_to_form", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Text for the button, up to 20 characters.", + validators=( + django.core.validators.MaxLengthValidator( + 20 + ), + ), + ), + ), + ( + "form", + wagtail.snippets.blocks.SnippetChooserBlock( + "home.Assessment", + help_text="Form the button should start", + ), + ), + ] + ), + ), + ], + max_num=3, + required=False, + ), + ), + ( + "list_title", + wagtail.blocks.CharBlock( + help_text="List title, up to 24 characters.", + max_length=24, + required=False, + ), + ), + ( + "list_items", + wagtail.blocks.StreamBlock( + [ + ( + "next_message", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Text for the list item, up to 24 characters.", + validators=( + django.core.validators.MaxLengthValidator( + 24 + ), + ), + ), + ) + ] + ), + ), + ( + "go_to_page", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Text for the list item, up to 24 characters.", + validators=( + django.core.validators.MaxLengthValidator( + 24 + ), + ), + ), + ), + ( + "page", + wagtail.blocks.PageChooserBlock( + help_text="Page the list item should go to" + ), + ), + ] + ), + ), + ( + "go_to_form", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Text for the list item, up to 24 characters.", + validators=( + django.core.validators.MaxLengthValidator( + 24 + ), + ), + ), + ), + ( + "form", + wagtail.snippets.blocks.SnippetChooserBlock( + "home.Assessment", + help_text="Form the list item should start", + ), + ), + ] + ), + ), + ], + help_text="Items to appear in the list message", + max_num=10, + required=False, + ), + ), + ( + "footer", + wagtail.blocks.CharBlock( + help_text="Footer cannot exceed 60 characters.", + required=False, + validators=( + django.core.validators.MaxLengthValidator( + 60 + ), + ), + ), + ), + ], + help_text="Each message will be sent with the text and media", + ), + ) + ], + blank=True, + null=True, + use_json_field=True, + ), + ), + ] diff --git a/home/models.py b/home/models.py index 94584f1e..253c1911 100644 --- a/home/models.py +++ b/home/models.py @@ -39,6 +39,7 @@ ) from wagtail.models.sites import Site from wagtail.search import index +from wagtail.snippets.blocks import SnippetChooserBlock from wagtail_content_import.models import ContentImportMixin from wagtailmedia.blocks import AbstractMediaChooserBlock @@ -241,6 +242,13 @@ class GoToPageButton(blocks.StructBlock): ) page = blocks.PageChooserBlock(help_text="Page the button should go to") +class GoToFormButton(blocks.StructBlock): + title = blocks.CharBlock( + help_text="Text for the button, up to 20 characters.", + validators=(MaxLengthValidator(20),), + ) + form = SnippetChooserBlock("home.Assessment", help_text="Form the button should start") + class NextMessageListItem(blocks.StructBlock): title = blocks.CharBlock( @@ -256,6 +264,13 @@ class GoToPageListItem(blocks.StructBlock): ) page = blocks.PageChooserBlock(help_text="Page the list item should go to") +class GoToFormListItem(blocks.StructBlock): + title = blocks.CharBlock( + help_text="Text for the list item, up to 24 characters.", + validators=(MaxLengthValidator(24),), + ) + form = SnippetChooserBlock("home.Assessment", help_text="Form the list item should start") + class WhatsappBlock(blocks.StructBlock): MEDIA_CAPTION_MAX_LENGTH = 1024 @@ -284,7 +299,7 @@ class WhatsappBlock(blocks.StructBlock): validators=(MaxLengthValidator(20),), ) buttons = blocks.StreamBlock( - [("next_message", NextMessageButton()), ("go_to_page", GoToPageButton())], + [("next_message", NextMessageButton()), ("go_to_page", GoToPageButton()), ("go_to_form", GoToFormButton())], required=False, max_num=3, ) @@ -294,7 +309,7 @@ class WhatsappBlock(blocks.StructBlock): max_length=24, ) list_items = blocks.StreamBlock( - [("next_message", NextMessageListItem()), ("go_to_page", GoToPageListItem())], + [("next_message", NextMessageListItem()), ("go_to_page", GoToPageListItem()), ("go_to_form", GoToFormListItem())], help_text="Items to appear in the list message", required=False, max_num=10, From 92aed996db33bd885132036772c3b017f714efa2 Mon Sep 17 00:00:00 2001 From: Rudi Giesler Date: Wed, 4 Dec 2024 15:57:14 +0200 Subject: [PATCH 2/7] Add import/export for go to form buttons and list items --- home/export_content_pages.py | 5 + home/import_content_pages.py | 48 ++++- home/models.py | 22 ++- .../missing-gotoform-list.csv | 2 + .../import-export-data/missing-gotoform.csv | 2 + home/tests/page_builder.py | 58 +++++- home/tests/test_api.py | 11 +- home/tests/test_content_import_export.py | 182 ++++++++++++++++-- 8 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 home/tests/import-export-data/missing-gotoform-list.csv create mode 100644 home/tests/import-export-data/missing-gotoform.csv diff --git a/home/export_content_pages.py b/home/export_content_pages.py index ce5a22ad..28427297 100644 --- a/home/export_content_pages.py +++ b/home/export_content_pages.py @@ -154,6 +154,11 @@ def serialise_buttons(buttons: blocks.StreamValue.StreamChild) -> str: if button.value.get("page") is None: continue button_dict["slug"] = button.value["page"].slug + if button.block_type == "go_to_form": + # Exclude buttons that has deleted forms that they are linked to it + if button.value.get("form") is None: + continue + button_dict["slug"] = button.value["form"].slug button_dicts.append(button_dict) return dumps(button_dicts) diff --git a/home/import_content_pages.py b/home/import_content_pages.py index 44faf376..95685ad0 100644 --- a/home/import_content_pages.py +++ b/home/import_content_pages.py @@ -25,6 +25,7 @@ from home.import_helpers import ImportException, validate_using_form from .models import ( + Assessment, ContentPage, ContentPageIndex, ContentQuickReply, @@ -189,10 +190,11 @@ def add_go_to_page_buttons(self) -> None: f"page '{slug}'", row.row_num, ) - page.whatsapp_body[message_index].value["buttons"].append( - ("go_to_page", {"page": related_page, "title": title}) + page.whatsapp_body[message_index].value["buttons"].insert( + button["index"], + ("go_to_page", {"page": related_page, "title": title}), ) - page.save_revision().publish() + page.save() def add_go_to_page_list_items(self) -> None: for (slug, locale), messages in self.go_to_page_list_items.items(): @@ -216,7 +218,7 @@ def add_go_to_page_list_items(self) -> None: item["index"], ("go_to_page", {"page": related_page, "title": title}), ) - page.save_revision().publish() + page.save() def parse_file(self) -> list["ContentRow"]: if self.file_type == "XLSX": @@ -373,7 +375,7 @@ def add_message_to_shadow_content_page_from_row( if row.is_whatsapp_message: page.enable_whatsapp = True buttons = [] - for button in row.buttons: + for index, button in enumerate(row.buttons): if button["type"] == "next_message": buttons.append( { @@ -383,8 +385,28 @@ def add_message_to_shadow_content_page_from_row( } ) elif button["type"] == "go_to_page": + button["index"] = index page_gtps = self.go_to_page_buttons[(row.slug, locale)] page_gtps[len(page.whatsapp_body)].append(button) + elif button["type"] == "go_to_form": + try: + form = Assessment.objects.get( + slug=button["slug"], locale=locale + ) + except Assessment.DoesNotExist: + raise ImportException( + f"No form found with slug '{button['slug']}' and locale " + f"'{locale}' for go_to_form button '{button['title']}' on " + f"page '{row.slug}'" + ) + buttons.append( + { + "id": str(uuid4()), + "type": button["type"], + "value": {"title": button["title"], "form": form.id}, + } + ) + list_items = [] for index, item in enumerate(row.list_items): if item["type"] == "next_message": @@ -399,6 +421,22 @@ def add_message_to_shadow_content_page_from_row( item["index"] = index page_gtpli = self.go_to_page_list_items[(row.slug, locale)] page_gtpli[len(page.whatsapp_body)].append(item) + elif item["type"] == "go_to_form": + try: + form = Assessment.objects.get(slug=item["slug"], locale=locale) + except Assessment.DoesNotExist: + raise ImportException( + f"No form found with slug '{item['slug']}' and locale " + f"'{locale}' for go_to_form list item '{item['title']}' on " + f"page '{row.slug}'" + ) + list_items.append( + { + "id": str(uuid4()), + "type": item["type"], + "value": {"title": item["title"], "form": form.id}, + } + ) page.whatsapp_body.append( ShadowWhatsappBlock( message=row.whatsapp_body, diff --git a/home/models.py b/home/models.py index 253c1911..d117bf31 100644 --- a/home/models.py +++ b/home/models.py @@ -242,12 +242,15 @@ class GoToPageButton(blocks.StructBlock): ) page = blocks.PageChooserBlock(help_text="Page the button should go to") + class GoToFormButton(blocks.StructBlock): title = blocks.CharBlock( help_text="Text for the button, up to 20 characters.", validators=(MaxLengthValidator(20),), ) - form = SnippetChooserBlock("home.Assessment", help_text="Form the button should start") + form = SnippetChooserBlock( + "home.Assessment", help_text="Form the button should start" + ) class NextMessageListItem(blocks.StructBlock): @@ -264,12 +267,15 @@ class GoToPageListItem(blocks.StructBlock): ) page = blocks.PageChooserBlock(help_text="Page the list item should go to") + class GoToFormListItem(blocks.StructBlock): title = blocks.CharBlock( help_text="Text for the list item, up to 24 characters.", validators=(MaxLengthValidator(24),), ) - form = SnippetChooserBlock("home.Assessment", help_text="Form the list item should start") + form = SnippetChooserBlock( + "home.Assessment", help_text="Form the list item should start" + ) class WhatsappBlock(blocks.StructBlock): @@ -299,7 +305,11 @@ class WhatsappBlock(blocks.StructBlock): validators=(MaxLengthValidator(20),), ) buttons = blocks.StreamBlock( - [("next_message", NextMessageButton()), ("go_to_page", GoToPageButton()), ("go_to_form", GoToFormButton())], + [ + ("next_message", NextMessageButton()), + ("go_to_page", GoToPageButton()), + ("go_to_form", GoToFormButton()), + ], required=False, max_num=3, ) @@ -309,7 +319,11 @@ class WhatsappBlock(blocks.StructBlock): max_length=24, ) list_items = blocks.StreamBlock( - [("next_message", NextMessageListItem()), ("go_to_page", GoToPageListItem()), ("go_to_form", GoToFormListItem())], + [ + ("next_message", NextMessageListItem()), + ("go_to_page", GoToPageListItem()), + ("go_to_form", GoToFormListItem()), + ], help_text="Items to appear in the list message", required=False, max_num=10, diff --git a/home/tests/import-export-data/missing-gotoform-list.csv b/home/tests/import-export-data/missing-gotoform-list.csv new file mode 100644 index 00000000..10dde29b --- /dev/null +++ b/home/tests/import-export-data/missing-gotoform-list.csv @@ -0,0 +1,2 @@ +structure,message,page_id,slug,parent,web_title,web_subtitle,web_body,whatsapp_title,whatsapp_body,whatsapp_template_name,whatsapp_template_category,example_values,variation_title,variation_body,sms_title,sms_body,messenger_title,messenger_body,viber_title,viber_body,translation_tag,tags,quick_replies,triggers,locale,next_prompt,list_items,image_link,doc_link,media_link,related_pages +Menu 1,0,659,ma_import-export,,MA_import export,,,Missing,GoToPage,,,,,,,,,,,,38a22433-e474-4f5a-b06b-7367d1a7f664,,,,English,,"[{""type"":""go_to_form"",""title"":""Missing"",""slug"":""missing""}]",,,, \ No newline at end of file diff --git a/home/tests/import-export-data/missing-gotoform.csv b/home/tests/import-export-data/missing-gotoform.csv new file mode 100644 index 00000000..e1a38cfc --- /dev/null +++ b/home/tests/import-export-data/missing-gotoform.csv @@ -0,0 +1,2 @@ +structure,message,page_id,slug,parent,web_title,web_subtitle,web_body,whatsapp_title,whatsapp_body,whatsapp_template_name,whatsapp_template_category,example_values,variation_title,variation_body,sms_title,sms_body,messenger_title,messenger_body,viber_title,viber_body,translation_tag,tags,quick_replies,triggers,locale,next_prompt,buttons,image_link,doc_link,media_link,related_pages +Menu 1,0,659,ma_import-export,,MA_import export,,,Missing,GoToPage,,,,,,,,,,,,38a22433-e474-4f5a-b06b-7367d1a7f664,,,,English,,"[{""type"":""go_to_form"",""title"":""Missing"",""slug"":""missing""}]",,,, \ No newline at end of file diff --git a/home/tests/page_builder.py b/home/tests/page_builder.py index cd9f6245..883991c8 100644 --- a/home/tests/page_builder.py +++ b/home/tests/page_builder.py @@ -7,6 +7,7 @@ from wagtail.models import Page # type: ignore from home.models import ( + Assessment, ContentPage, ContentPageIndex, ContentQuickReply, @@ -69,6 +70,54 @@ def value_dict(self) -> dict[str, Any]: return asdict(self) | {"page": self.page.id} +@dataclass +class FormBtn(Btn): + BLOCK_TYPE_STR = "go_to_form" + + form: Assessment + + def value_dict(self) -> dict[str, Any]: + return asdict(self) | {"form": self.form.id} + + +@dataclass +class ListItem: + BLOCK_TYPE_STR: ClassVar[str] + + title: str + + def value_dict(self) -> dict[str, Any]: + return asdict(self) + + def to_dict(self) -> dict[str, Any]: + return {"type": self.BLOCK_TYPE_STR, "value": self.value_dict()} + + +@dataclass +class NextListItem(ListItem): + BLOCK_TYPE_STR = "next_message" + + +@dataclass +class PageListItem(ListItem): + BLOCK_TYPE_STR = "go_to_page" + + page: Page + + def value_dict(self) -> dict[str, Any]: + return asdict(self) | {"page": self.page.id} + + +@dataclass +class FormListItem(ListItem): + BLOCK_TYPE_STR = "go_to_form" + + form: Assessment + + def value_dict(self) -> dict[str, Any]: + return asdict(self) | {"form": self.form.id} + + @dataclass class ContentBlock: BLOCK_TYPE_STR: ClassVar[str] @@ -113,7 +162,7 @@ class WABlk(ContentBlock): example_values: list[str] = field(default_factory=list) buttons: list[Btn] = field(default_factory=list) list_title: str = "" - list_items: list[dict[str, Any]] = field(default_factory=list) + list_items: list[ListItem] = field(default_factory=list) media: int | None = None document: str | None = None footer: str = "" @@ -121,7 +170,12 @@ class WABlk(ContentBlock): def to_dict(self) -> dict[str, Any]: varmsgs = [vm.to_dict() for vm in self.variation_messages] buttons = [b.to_dict() for b in self.buttons] - return super().to_dict() | {"variation_messages": varmsgs, "buttons": buttons} + list_items = [li.to_dict() for li in self.list_items] + return super().to_dict() | { + "variation_messages": varmsgs, + "buttons": buttons, + "list_items": list_items, + } @dataclass diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 4428d514..a9feaf44 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -38,6 +38,7 @@ MBlk, MBody, NextBtn, + NextListItem, PageBuilder, SBlk, SBody, @@ -952,10 +953,7 @@ def test_list_items_no_title(self, uclient): test that list items are present in the whatsapp message with no title given """ page = self.create_content_page( - list_items=[ - {"type": "next_message", "value": {"title": "list item 1"}}, - {"type": "next_message", "value": {"title": "list item 2"}}, - ] + list_items=[NextListItem("list item 1"), NextListItem("list item 2")] ) response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=true") @@ -975,10 +973,7 @@ def test_list_items(self, uclient): """ page = self.create_content_page( list_title="List Title", - list_items=[ - {"type": "next_message", "value": {"title": "list item 1"}}, - {"type": "next_message", "value": {"title": "list item 2"}}, - ], + list_items=[NextListItem("list item 1"), NextListItem("list item 2")], ) response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=true") diff --git a/home/tests/test_content_import_export.py b/home/tests/test_content_import_export.py index 03581fd6..d74630da 100644 --- a/home/tests/test_content_import_export.py +++ b/home/tests/test_content_import_export.py @@ -23,8 +23,11 @@ from home.content_import_export import import_content, import_ordered_sets from home.import_helpers import ImportException from home.models import ( + Assessment, ContentPage, ContentPageIndex, + GoToFormButton, + GoToFormListItem, GoToPageButton, HomePage, OrderedContentSet, @@ -33,11 +36,15 @@ from .helpers import set_profile_field_options from .page_builder import ( + FormBtn, + FormListItem, MBlk, MBody, NextBtn, + NextListItem, PageBtn, PageBuilder, + PageListItem, SBlk, SBody, UBlk, @@ -188,6 +195,22 @@ def _normalise_button_pks(body: DbDict, min_pk: int) -> DbDict: button = button | {"value": v | {"page": v["page"] - min_pk}} buttons.append(button) value = value | {"buttons": buttons} + print("nomralised bttons ", body | {"value": value}) + return body | {"value": value} + + +def _normalise_list_pks(body: DbDict, min_pk: int) -> DbDict: + value = body["value"] + if "list_items" in value: + list_items = [] + for list_item in value["list_items"]: + if list_item["type"] == "go_to_page": + if list_item.get("value").get("page") is None: + continue + v = list_item["value"] + list_item = list_item | {"value": v | {"page": v["page"] - min_pk}} + list_items.append(list_item) + value = value | {"list_items": list_items} return body | {"value": value} @@ -200,6 +223,7 @@ def _normalise_pks(page: DbDict, min_pk: int) -> DbDict: fields = fields | {"related_pages": related_pages} if "whatsapp_body" in fields: body = [_normalise_button_pks(b, min_pk) for b in fields["whatsapp_body"]] + body = [_normalise_list_pks(b, min_pk) for b in body] fields = fields | {"whatsapp_body": body} return page | {"fields": fields, "pk": page["pk"] - min_pk} @@ -931,6 +955,34 @@ def test_go_to_page_button_missing_page(self, csv_impexp: ImportExport) -> None: "button 'Missing' on page 'ma_import-export'" ] + def test_go_to_form_button_missing_form(self, csv_impexp: ImportExport) -> None: + """ + Go to form buttons in the import file link to other forms using the slug. But + if no form with that slug exists, then we should give the user an error message + that tells them where and how to fix it. + """ + with pytest.raises(ImportException) as e: + csv_impexp.import_file("missing-gotoform.csv") + assert e.value.row_num == 2 + assert e.value.message == [ + "No form found with slug 'missing' and locale 'English' for go_to_form " + "button 'Missing' on page 'ma_import-export'" + ] + + def test_go_to_form_list_missing_form(self, csv_impexp: ImportExport) -> None: + """ + Go to form list items buttons in the import file link to other forms using the + slug. But if no form with that slug exists, then we should give the user an + error message that tells them where and how to fix it. + """ + with pytest.raises(ImportException) as e: + csv_impexp.import_file("missing-gotoform-list.csv") + assert e.value.row_num == 2 + assert e.value.message == [ + "No form found with slug 'missing' and locale 'English' for go_to_form " + "list item 'Missing' on page 'ma_import-export'" + ] + def test_missing_related_pages(self, csv_impexp: ImportExport) -> None: """ Related pages are listed as comma separated slugs in imported files. If there @@ -1755,6 +1807,16 @@ def add_go_to_page_button(whatsapp_block: Any, button: PageBtn) -> None: whatsapp_block.value["buttons"].append(("go_to_page", button_val)) +def add_go_to_form_button(whatsapp_block: Any, button: FormBtn) -> None: + button_val = GoToFormButton().to_python(button.value_dict()) + whatsapp_block.value["buttons"].append(("go_to_form", button_val)) + + +def add_go_to_form_list_item(whatsapp_block: Any, list_item: FormListItem) -> None: + list_val = GoToFormListItem().to_python(list_item.value_dict()) + whatsapp_block.value["list_items"].append(("go_to_form", list_val)) + + @pytest.mark.usefixtures("tmp_media_path") @pytest.mark.django_db class TestExportImportRoundtrip: @@ -1932,17 +1994,13 @@ def test_variations(self, impexp: ImportExport) -> None: ), WABlk("Message 3 with no variation", next_prompt="Next message"), ] - cp_imp_exp = PageBuilder.build_cp( + _cp_imp_exp = PageBuilder.build_cp( parent=imp_exp, slug="cp-import-export", title="CP-Import/export", bodies=[WABody("WA import export data", cp_imp_exp_wablks)], ) - # Save and publish cp_imp_exp again so the revision numbers match up after import. - rev = cp_imp_exp.save_revision() - rev.publish() - orig = impexp.get_page_json() impexp.export_reimport() imported = impexp.get_page_json() @@ -2519,9 +2577,13 @@ def test_list_items(self, impexp: ImportExport) -> None: bodies=[WABody("HealthAlert menu", [WABlk("*Welcome to HealthAlert* WA")])], ) + form = Assessment.objects.create( + title="Test form", slug="test-form", locale=home_page.locale + ) list_items = [ - {"type": "next_message", "value": {"title": "Item 1"}}, - {"type": "next_message", "value": {"title": "Item 2"}}, + NextListItem("Item 1"), + PageListItem("Item 2", page=ha_menu), + FormListItem("Item 3", form=form), ] _health_info = PageBuilder.build_cp( parent=ha_menu, @@ -2572,7 +2634,7 @@ def test_export_import_page_with_go_to_button(self, impexp: ImportExport) -> Non ], ) - self_help = PageBuilder.build_cp( + _self_help = PageBuilder.build_cp( parent=ha_menu, slug="self-help", title="self-help", @@ -2589,9 +2651,6 @@ def test_export_import_page_with_go_to_button(self, impexp: ImportExport) -> Non ], ) - rev = self_help.save_revision() - rev.publish() - orig = impexp.get_page_json() impexp.export_reimport() imported = impexp.get_page_json() @@ -2650,3 +2709,104 @@ def test_export_missing_go_to_button(self, impexp: ImportExport) -> None: updated_json = impexp.get_page_json() assert orig_json == updated_json + + def test_buttons(self, impexp: ImportExport) -> None: + """ + Content page buttons should import and export + """ + home_page = HomePage.objects.first() + main_menu = PageBuilder.build_cpi(home_page, "main-menu", "Main Menu") + + form = Assessment.objects.create( + title="Test form", slug="test-form", locale=home_page.locale + ) + + target_page = PageBuilder.build_cp( + parent=main_menu, + slug="target_page", + title="Target page", + bodies=[WABody("Target", [WABlk("Target page")])], + ) + + PageBuilder.build_cp( + parent=main_menu, + slug="ha-menu", + title="HealthAlert menu", + bodies=[ + WABody( + "HealthAlert menu", + [ + WABlk( + "*Welcome to HealthAlert*", + buttons=[ + NextBtn("Go to next page"), + PageBtn("Go to page", page=target_page), + FormBtn("Start form", form=form), + ], + ) + ], + ) + ], + ) + orig = impexp.get_page_json() + impexp.export_reimport() + imported = impexp.get_page_json() + assert imported == orig + + def test_export_import_page_with_missing_form_button( + self, impexp: ImportExport + ) -> None: + """ + If a go_to_form button links to a delete form, it should be excluded from exports + """ + home_page = HomePage.objects.first() + main_menu = PageBuilder.build_cpi(home_page, "main-menu", "Main Menu") + + page = PageBuilder.build_cp( + parent=main_menu, + slug="ha-menu", + title="HealthAlert menu", + bodies=[WABody("HealthAlert menu", [WABlk("*Welcome to HealthAlert*")])], + ) + orig = impexp.get_page_json() + + # Add another button to the page, then delete the form it links to + form = Assessment.objects.create( + title="Test form", slug="test-form", locale=home_page.locale + ) + add_go_to_form_button(page.whatsapp_body[0], FormBtn("Go to Btn_2", form=form)) + form.delete() + + impexp.export_reimport() + imported = impexp.get_page_json() + assert imported == orig + + def test_export_import_page_with_missing_form_list( + self, impexp: ImportExport + ) -> None: + """ + If a go_to_form list item links to a delete form, it should be excluded from exports + """ + home_page = HomePage.objects.first() + main_menu = PageBuilder.build_cpi(home_page, "main-menu", "Main Menu") + + page = PageBuilder.build_cp( + parent=main_menu, + slug="ha-menu", + title="HealthAlert menu", + bodies=[WABody("HealthAlert menu", [WABlk("*Welcome to HealthAlert*")])], + ) + orig = impexp.get_page_json() + + # Add another button to the page, then delete the form it links to + form = Assessment.objects.create( + title="Test form", slug="test-form", locale=home_page.locale + ) + add_go_to_form_list_item( + page.whatsapp_body[0], FormListItem("Go to Btn_2", form=form) + ) + form.delete() + + impexp.export_reimport() + imported = impexp.get_page_json() + assert imported == orig From cddbf8d15fe144d6ccb6c97f7d2e399b7dd2842e Mon Sep 17 00:00:00 2001 From: Rudi Giesler Date: Wed, 4 Dec 2024 17:48:51 +0200 Subject: [PATCH 3/7] Add go to form button and list items to API --- home/tests/test_api.py | 108 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 10 deletions(-) diff --git a/home/tests/test_api.py b/home/tests/test_api.py index a9feaf44..8b51857c 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -35,11 +35,15 @@ ) from .page_builder import ( + FormBtn, + FormListItem, MBlk, MBody, NextBtn, NextListItem, + PageBtn, PageBuilder, + PageListItem, SBlk, SBody, UBlk, @@ -881,15 +885,58 @@ def create_content_page( def test_whatsapp_detail_view_with_button(self, uclient): """ - Next page buttons in WhatsApp messages are present in the message body. + Buttons in WhatsApp messages are present in the message body. """ - page = self.create_content_page(buttons=[NextBtn("Tell me more")]) + home_page = HomePage.objects.first() + main_menu = PageBuilder.build_cpi(home_page, "main-menu", "Main Menu") + target_page = PageBuilder.build_cp( + parent=main_menu, slug="target-page", title="Target page", bodies=[] + ) + form = Assessment.objects.create( + title="Test form", slug="test-form", locale=target_page.locale + ) + + page = PageBuilder.build_cp( + parent=main_menu, + slug="page", + title="Page", + bodies=[ + WABody( + "Page", + [ + WABlk( + "Button message", + buttons=[ + NextBtn("Tell me more"), + PageBtn("Go elsewhere", page=target_page), + FormBtn("Start form", form=form), + ], + ) + ], + ) + ], + ) response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=true&message=1") content = response.json() - [button] = content["body"]["text"]["value"]["buttons"] - button.pop("id") - assert button == {"type": "next_message", "value": {"title": "Tell me more"}} + [next_button, page_button, form_button] = content["body"]["text"]["value"][ + "buttons" + ] + next_button.pop("id") + assert next_button == { + "type": "next_message", + "value": {"title": "Tell me more"}, + } + page_button.pop("id") + assert page_button == { + "type": "go_to_page", + "value": {"title": "Go elsewhere", "page": target_page.id}, + } + form_button.pop("id") + assert form_button == { + "type": "go_to_form", + "value": {"title": "Start form", "form": form.id}, + } def test_whatsapp_template_fields(self, uclient): """ @@ -971,21 +1018,62 @@ def test_list_items(self, uclient): """ test that list items are present in the whatsapp message """ - page = self.create_content_page( - list_title="List Title", - list_items=[NextListItem("list item 1"), NextListItem("list item 2")], + home_page = HomePage.objects.first() + main_menu = PageBuilder.build_cpi(home_page, "main-menu", "Main Menu") + target_page = PageBuilder.build_cp( + parent=main_menu, slug="target-page", title="Target page", bodies=[] + ) + form = Assessment.objects.create( + title="Test form", slug="test-form", locale=target_page.locale + ) + + page = PageBuilder.build_cp( + parent=main_menu, + slug="page", + title="Page", + bodies=[ + WABody( + "list body", + [ + WABlk( + "List message", + list_items=[ + NextListItem("list item 1"), + PageListItem("list item 2", page=target_page), + FormListItem("list item 3", form=form), + ], + ) + ], + ) + ], ) response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=true") content = response.json() - [item_1, item_2] = content["body"]["text"]["value"]["list_items"] + [item_1, item_2, item_3] = content["body"]["text"]["value"]["list_items"] item_1.pop("id") item_2.pop("id") + item_3.pop("id") - assert content["body"]["text"]["value"]["list_title"] == "List Title" assert item_1 == {"type": "item", "value": "list item 1"} assert item_2 == {"type": "item", "value": "list item 2"} + assert item_3 == {"type": "item", "value": "list item 3"} + + [item_1, item_2, item_3] = content["body"]["text"]["value"]["list_items_v2"] + item_1.pop("id") + item_2.pop("id") + item_3.pop("id") + + assert item_1 == {"type": "next_message", "value": {"title": "list item 1"}} + assert item_2 == { + "type": "go_to_page", + "value": {"title": "list item 2", "page": target_page.id}, + } + assert item_3 == { + "type": "go_to_form", + "value": {"title": "list item 3", "form": form.id}, + } def test_next_prompt(self, uclient): """ From 3f7c80dbeef852a4f242f3fba9b6f2db754f7df6 Mon Sep 17 00:00:00 2001 From: Rudi Giesler Date: Wed, 4 Dec 2024 17:56:33 +0200 Subject: [PATCH 4/7] Update changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a00963f..9a5f0d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## v1.3.0 From f6b85ec5468a450f8f076920f8d498358a3e36a2 Mon Sep 17 00:00:00 2001 From: Rudi Giesler Date: Thu, 5 Dec 2024 12:39:47 +0200 Subject: [PATCH 5/7] Remove debug print statement --- home/tests/test_content_import_export.py | 1 - 1 file changed, 1 deletion(-) diff --git a/home/tests/test_content_import_export.py b/home/tests/test_content_import_export.py index d74630da..0b090f15 100644 --- a/home/tests/test_content_import_export.py +++ b/home/tests/test_content_import_export.py @@ -195,7 +195,6 @@ def _normalise_button_pks(body: DbDict, min_pk: int) -> DbDict: button = button | {"value": v | {"page": v["page"] - min_pk}} buttons.append(button) value = value | {"buttons": buttons} - print("nomralised bttons ", body | {"value": value}) return body | {"value": value} From b0e0ff2debc51c5ef5ab0e51820f6155aede458b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:57:19 +0000 Subject: [PATCH 6/7] Bump django from 4.2.16 to 4.2.17 Bumps [django](https://github.com/django/django) from 4.2.16 to 4.2.17. - [Commits](https://github.com/django/django/compare/4.2.16...4.2.17) --- updated-dependencies: - dependency-name: django dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 57f35bc8..ca8d4914 100644 --- a/poetry.lock +++ b/poetry.lock @@ -390,13 +390,13 @@ typing-extensions = ">=3.10.0.0" [[package]] name = "django" -version = "4.2.16" +version = "4.2.17" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.8" files = [ - {file = "Django-4.2.16-py3-none-any.whl", hash = "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898"}, - {file = "Django-4.2.16.tar.gz", hash = "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad"}, + {file = "Django-4.2.17-py3-none-any.whl", hash = "sha256:3a93350214ba25f178d4045c0786c61573e7dbfa3c509b3551374f1e11ba8de0"}, + {file = "Django-4.2.17.tar.gz", hash = "sha256:6b56d834cc94c8b21a8f4e775064896be3b4a4ca387f2612d4406a5927cd2fdc"}, ] [package.dependencies] @@ -2005,4 +2005,4 @@ testing = ["Pillow (>=9.1.0,<11.0.0)", "Wand (>=0.6,<1.0)", "black (==22.3.0)", [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "5042a2b58af18134861b3937949b1541b8d56d215cb381f2bea99eb30592fabd" +content-hash = "13423dfde948ce5494b570bf20d293fdf24270f06fae78e1889fb8af0e73b820" diff --git a/pyproject.toml b/pyproject.toml index ba3d51dc..bd6a130c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" wagtail = "~5.2.6" # Latest LTS -django = "~4.2.16" # Latest LTS +django = "~4.2.17" # Latest LTS dj-database-url = "^2.1.0" psycopg2-binary = "^2.9.9" django-environ = "^0.11.2" From 4cc29f80a2e27606308810248022f4bc9256a3cd Mon Sep 17 00:00:00 2001 From: Rudi Giesler Date: Mon, 9 Dec 2024 08:03:22 +0200 Subject: [PATCH 7/7] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a00963f..2660872c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ContentPage import: Accept xlsx where field formatting is numeric. - Validate OrderedContentSets on import - Forms import: Error message for differing number of answer items for different fields +### Security +- Updated django from 4.2.16 to 4.2.17 --> ## v1.3.0