From 0d14a0689828d656a52e29e3a7b9b4aa8eec6d63 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Tue, 6 Feb 2024 16:10:08 +0200 Subject: [PATCH 01/21] Add draft status and rivisions for ordered content sets --- ...49_orderedcontentset_expire_at_and_more.py | 89 +++++++++++++++++++ home/models.py | 6 +- 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 home/migrations/0049_orderedcontentset_expire_at_and_more.py diff --git a/home/migrations/0049_orderedcontentset_expire_at_and_more.py b/home/migrations/0049_orderedcontentset_expire_at_and_more.py new file mode 100644 index 00000000..e0d7d0fb --- /dev/null +++ b/home/migrations/0049_orderedcontentset_expire_at_and_more.py @@ -0,0 +1,89 @@ +# Generated by Django 4.1.13 on 2024-02-06 12:25 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("home", "0048_alter_contentpage_whatsapp_body"), + ] + + operations = [ + migrations.AddField( + model_name="orderedcontentset", + name="expire_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="expiry date/time" + ), + ), + migrations.AddField( + model_name="orderedcontentset", + name="expired", + field=models.BooleanField( + default=False, editable=False, verbose_name="expired" + ), + ), + migrations.AddField( + model_name="orderedcontentset", + name="first_published_at", + field=models.DateTimeField( + blank=True, db_index=True, null=True, verbose_name="first published at" + ), + ), + migrations.AddField( + model_name="orderedcontentset", + name="go_live_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="go live date/time" + ), + ), + migrations.AddField( + model_name="orderedcontentset", + name="has_unpublished_changes", + field=models.BooleanField( + default=False, editable=False, verbose_name="has unpublished changes" + ), + ), + migrations.AddField( + model_name="orderedcontentset", + name="last_published_at", + field=models.DateTimeField( + editable=False, null=True, verbose_name="last published at" + ), + ), + migrations.AddField( + model_name="orderedcontentset", + name="latest_revision", + field=models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailcore.revision", + verbose_name="latest revision", + ), + ), + migrations.AddField( + model_name="orderedcontentset", + name="live", + field=models.BooleanField( + default=True, editable=False, verbose_name="live" + ), + ), + migrations.AddField( + model_name="orderedcontentset", + name="live_revision", + field=models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailcore.revision", + verbose_name="live revision", + ), + ), + ] diff --git a/home/models.py b/home/models.py index 21482055..a9e889b5 100644 --- a/home/models.py +++ b/home/models.py @@ -1,6 +1,7 @@ import re from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MaxLengthValidator from django.db import models @@ -18,7 +19,7 @@ from wagtail.documents.blocks import DocumentChooserBlock from wagtail.fields import StreamField from wagtail.images.blocks import ImageChooserBlock -from wagtail.models import Page, Revision +from wagtail.models import DraftStateMixin, Page, Revision, RevisionMixin from wagtail.models.sites import Site from wagtail.search import index from wagtail_content_import.models import ContentImportMixin @@ -983,7 +984,8 @@ def update_embedding(sender, instance, *args, **kwargs): instance.embedding = embedding -class OrderedContentSet(index.Indexed, models.Model): +class OrderedContentSet(DraftStateMixin, RevisionMixin, index.Indexed, models.Model): + revisions = GenericRelation("wagtailcore.Revision", related_query_name="orderedcontentset") name = models.CharField( max_length=255, help_text="The name of the ordered content set." ) From cc9136e35a95accd0edf5f0f3464d327dc98660e Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Wed, 7 Feb 2024 11:24:53 +0200 Subject: [PATCH 02/21] Register as snippet --- home/models.py | 5 +++-- home/wagtail_hooks.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/home/models.py b/home/models.py index a9e889b5..64b3cffe 100644 --- a/home/models.py +++ b/home/models.py @@ -983,9 +983,10 @@ def update_embedding(sender, instance, *args, **kwargs): instance.embedding = embedding - class OrderedContentSet(DraftStateMixin, RevisionMixin, index.Indexed, models.Model): - revisions = GenericRelation("wagtailcore.Revision", related_query_name="orderedcontentset") + revisions = GenericRelation( + "wagtailcore.Revision", related_query_name="orderedcontentset" + ) name = models.CharField( max_length=255, help_text="The name of the ordered content set." ) diff --git a/home/wagtail_hooks.py b/home/wagtail_hooks.py index 1725ea14..5c97ff52 100644 --- a/home/wagtail_hooks.py +++ b/home/wagtail_hooks.py @@ -4,6 +4,7 @@ from wagtail.admin import widgets as wagtailadmin_widgets from wagtail.admin.menu import AdminOnlyMenuItem from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register +from wagtail.snippets.models import register_snippet from .models import ContentPage, OrderedContentSet @@ -15,6 +16,7 @@ ContentUploadView, ) +register_snippet(OrderedContentSet) @hooks.register("register_admin_urls") def register_import_urls(): From e7f01f3a1932dc7f7c0e5383f760a9eb8bcbd37c Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Wed, 7 Feb 2024 12:08:35 +0200 Subject: [PATCH 03/21] Migrate ModelAdmin to SnippetViewSet --- home/models.py | 3 +++ home/wagtail_hooks.py | 11 +++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/home/models.py b/home/models.py index 64b3cffe..8aac3fd8 100644 --- a/home/models.py +++ b/home/models.py @@ -1073,6 +1073,9 @@ def get_relationship(self): null=True, ) + def num_pages(self): + return len(self.pages) + panels = [ FieldPanel("name"), FieldPanel("profile_fields"), diff --git a/home/wagtail_hooks.py b/home/wagtail_hooks.py index 5c97ff52..b2545bce 100644 --- a/home/wagtail_hooks.py +++ b/home/wagtail_hooks.py @@ -5,6 +5,7 @@ from wagtail.admin.menu import AdminOnlyMenuItem from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register from wagtail.snippets.models import register_snippet +from wagtail.snippets.views.snippets import SnippetViewSet from .models import ContentPage, OrderedContentSet @@ -16,8 +17,6 @@ ContentUploadView, ) -register_snippet(OrderedContentSet) - @hooks.register("register_admin_urls") def register_import_urls(): return [ @@ -173,10 +172,11 @@ def parental(self, obj): parental.short_description = "Parent" -class OrderedContentSetAdmin(ModelAdmin): +class OrderedContentSetViewSet(SnippetViewSet): model = OrderedContentSet - menu_icon = "order" + icon = "order" menu_order = 200 + add_to_admin_menu = True add_to_settings_menu = False exclude_from_explorer = False list_display = ("name", "profile_fields", "num_pages") @@ -207,7 +207,6 @@ def num_pages(self, obj): num_pages.short_description = "Number of Pages" - +register_snippet(OrderedContentSetViewSet) # Now you just need to register your customised ModelAdmin class with Wagtail modeladmin_register(ContentPageAdmin) -modeladmin_register(OrderedContentSetAdmin) From b634ce886a8f5c008a13e436f628b0ca637cba1d Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Wed, 7 Feb 2024 16:24:42 +0200 Subject: [PATCH 04/21] Filter draft content sets from api --- home/api.py | 11 ++++++- home/tests/test_api.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/home/api.py b/home/api.py index a4bf6cb8..876beacc 100644 --- a/home/api.py +++ b/home/api.py @@ -147,11 +147,20 @@ class OrderedContentSetViewSet(BaseAPIViewSet): "name", "profile_fields", ] - known_query_parameters = BaseAPIViewSet.known_query_parameters.union(["page"]) + known_query_parameters = BaseAPIViewSet.known_query_parameters.union(["page", "qa"]) pagination_class = PageNumberPagination search_fields = ["name", "profile_fields"] filter_backends = (SearchFilter,) + def get_queryset(self): + qa = self.request.query_params.get("qa") + + if qa: + queryset = OrderedContentSet.objects.all() + else: + queryset = OrderedContentSet.objects.filter(live=True) + return queryset + api_router = WagtailAPIRouter("wagtailapi") diff --git a/home/tests/test_api.py b/home/tests/test_api.py index f09b7369..69ee67ee 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -610,6 +610,72 @@ def test_orderedcontent_detail_endpoint_tags_flag(self, uclient): } assert content["pages"][0]["tags"] == [t.name for t in self.page1.tags.all()] + def test_orderedcontent_endpoint_with_drafts(self, uclient): + """ + Unpublished ordered content sets are returned if the qa param is set. + """ + self.ordered_content_set.unpublish() + url = "/api/v2/orderedcontent/?qa=True" + # it should return a list of ordered content sets with the unpublished one included + response = uclient.get(url) + content = json.loads(response.content) + + # the content set is not live but content is returned + assert not self.ordered_content_set.live + assert content["count"] == 2 + assert content["results"][0]["name"] == self.ordered_content_set.name + assert content["results"][0]["profile_fields"][0] == { + "profile_field": "gender", + "value": "female", + } + def test_orderedcontent_endpoint_without_drafts(self, uclient): + """ + Unpublished ordered content sets are not returned if the qa param is not set. + """ + self.ordered_content_set.unpublish() + url = "/api/v2/orderedcontent/" + # it should return a list of ordered content sets with the unpublished one excluded + response = uclient.get(url) + content = json.loads(response.content) + + # the content set is not live but content is returned + assert not self.ordered_content_set.live + assert content["count"] == 1 + assert content["results"][0]["name"] == self.ordered_content_set_timed.name + assert content["results"][0]["profile_fields"][0] == { + "profile_field": "gender", + "value": "female", + } + + def test_orderedcontent_detail_endpoint_with_drafts(self, uclient): + """ + Unpublished ordered content sets are returned if the qa param is set. + """ + self.ordered_content_set.unpublish() + url = f"/api/v2/orderedcontent/{self.ordered_content_set.id}/?qa=True" + # it should return specific ordered content set that is in draft + response = uclient.get(url) + content = json.loads(response.content) + + # the content set is not live but content is returned + assert not self.ordered_content_set.live + assert content["name"] == self.ordered_content_set.name + assert content["profile_fields"][0] == { + "profile_field": "gender", + "value": "female", + } + + def test_orderedcontent_detail_endpoint_without_drafts(self, uclient): + """ + Unpublished ordered content sets are not returned if the qa param is not set. + """ + self.ordered_content_set.unpublish() + url = f"/api/v2/orderedcontent/{self.ordered_content_set.id}" + # it should return nothing + response = uclient.get(url) + + # it redirects :TODO is it possible to resolve the redirect? + assert response.status_code == 301 @pytest.mark.django_db class TestContentPageAPI2: From cc1030894721d568d566d03ee030540ce800cc15 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Thu, 8 Feb 2024 09:22:43 +0200 Subject: [PATCH 05/21] Black format --- home/models.py | 1 + home/tests/test_api.py | 2 ++ home/wagtail_hooks.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/home/models.py b/home/models.py index 8aac3fd8..52d432e5 100644 --- a/home/models.py +++ b/home/models.py @@ -983,6 +983,7 @@ def update_embedding(sender, instance, *args, **kwargs): instance.embedding = embedding + class OrderedContentSet(DraftStateMixin, RevisionMixin, index.Indexed, models.Model): revisions = GenericRelation( "wagtailcore.Revision", related_query_name="orderedcontentset" diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 69ee67ee..074deea2 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -628,6 +628,7 @@ def test_orderedcontent_endpoint_with_drafts(self, uclient): "profile_field": "gender", "value": "female", } + def test_orderedcontent_endpoint_without_drafts(self, uclient): """ Unpublished ordered content sets are not returned if the qa param is not set. @@ -677,6 +678,7 @@ def test_orderedcontent_detail_endpoint_without_drafts(self, uclient): # it redirects :TODO is it possible to resolve the redirect? assert response.status_code == 301 + @pytest.mark.django_db class TestContentPageAPI2: """ diff --git a/home/wagtail_hooks.py b/home/wagtail_hooks.py index b2545bce..f3b0e88d 100644 --- a/home/wagtail_hooks.py +++ b/home/wagtail_hooks.py @@ -17,6 +17,7 @@ ContentUploadView, ) + @hooks.register("register_admin_urls") def register_import_urls(): return [ @@ -207,6 +208,7 @@ def num_pages(self, obj): num_pages.short_description = "Number of Pages" + register_snippet(OrderedContentSetViewSet) # Now you just need to register your customised ModelAdmin class with Wagtail modeladmin_register(ContentPageAdmin) From 9882b9d04a3b28ec0b85dc9bc9fcfce8bcd48d7d Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Thu, 8 Feb 2024 12:30:19 +0200 Subject: [PATCH 06/21] Use latest revision for display and add status column --- home/models.py | 24 +++++++++++++++++++++--- home/wagtail_hooks.py | 12 +----------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/home/models.py b/home/models.py index 52d432e5..797f9e37 100644 --- a/home/models.py +++ b/home/models.py @@ -993,20 +993,33 @@ class OrderedContentSet(DraftStateMixin, RevisionMixin, index.Indexed, models.Mo ) def get_gender(self): - for item in self.profile_fields.raw_data: + for item in self.get_latest_revision_as_object().profile_fields.raw_data: if item["type"] == "gender": return item["value"] def get_age(self): - for item in self.profile_fields.raw_data: + for item in self.get_latest_revision_as_object().profile_fields.raw_data: if item["type"] == "age": return item["value"] def get_relationship(self): - for item in self.profile_fields.raw_data: + for item in self.get_latest_revision_as_object().profile_fields.raw_data: if item["type"] == "relationship": return item["value"] + def profile_field(self): + return [ + f"{x.block_type}:{x.value}" + for x in self.get_latest_revision_as_object().profile_fields + ] + + profile_field.short_description = "Profile Fields" + + def latest_draft_profile_fields(self): + return self.get_latest_revision_as_object().profile_fields + + latest_draft_profile_fields.short_description = "Profile Fields" + profile_fields = StreamField( [ ("gender", blocks.ChoiceBlock(choices=get_gender_choices)), @@ -1077,6 +1090,11 @@ def get_relationship(self): def num_pages(self): return len(self.pages) + num_pages.short_description = "Number of Pages" + + def status(self): + return "Live" if self.live else "Draft" + panels = [ FieldPanel("name"), FieldPanel("profile_fields"), diff --git a/home/wagtail_hooks.py b/home/wagtail_hooks.py index f3b0e88d..f9dbde1a 100644 --- a/home/wagtail_hooks.py +++ b/home/wagtail_hooks.py @@ -180,15 +180,10 @@ class OrderedContentSetViewSet(SnippetViewSet): add_to_admin_menu = True add_to_settings_menu = False exclude_from_explorer = False - list_display = ("name", "profile_fields", "num_pages") + list_display = ("name", "latest_draft_profile_fields", "num_pages", "status") list_export = ("name", "profile_field", "page") search_fields = ("name", "profile_fields") - def profile_field(self, obj): - return [f"{x.block_type}:{x.value}" for x in obj.profile_fields] - - profile_field.short_description = "Profile Fields" - def page(self, obj): if obj.pages: return [ @@ -203,11 +198,6 @@ def page(self, obj): page.short_description = "Page Slugs" - def num_pages(self, obj): - return len(obj.pages) - - num_pages.short_description = "Number of Pages" - register_snippet(OrderedContentSetViewSet) # Now you just need to register your customised ModelAdmin class with Wagtail From 2729058182bfef98bb9685c5ebdada1c0765ff59 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Thu, 15 Feb 2024 14:08:06 +0200 Subject: [PATCH 07/21] Simplify API for now --- home/api.py | 10 ++-- home/tests/test_api.py | 115 +++++++++++++++++++++++++++-------------- 2 files changed, 82 insertions(+), 43 deletions(-) diff --git a/home/api.py b/home/api.py index 876beacc..a2617161 100644 --- a/home/api.py +++ b/home/api.py @@ -1,3 +1,5 @@ + +from django.db.models.manager import BaseManager from rest_framework.exceptions import ValidationError from rest_framework.filters import SearchFilter from rest_framework.pagination import PageNumberPagination @@ -153,12 +155,10 @@ class OrderedContentSetViewSet(BaseAPIViewSet): filter_backends = (SearchFilter,) def get_queryset(self): - qa = self.request.query_params.get("qa") + #TODO: Filter using the qa Param + #qa = self.request.query_params.get("qa") - if qa: - queryset = OrderedContentSet.objects.all() - else: - queryset = OrderedContentSet.objects.filter(live=True) + queryset = OrderedContentSet.objects.all() return queryset diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 074deea2..95bfa1f6 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -481,12 +481,14 @@ def create_test_data(self): with path.open(mode="rb") as f: import_content(f, "CSV", queue.Queue()) self.page1 = ContentPage.objects.first() + # self.page1.save_revision().publish() self.ordered_content_set = OrderedContentSet(name="Test set") self.ordered_content_set.pages.append(("pages", {"contentpage": self.page1})) self.ordered_content_set.profile_fields.append(("gender", "female")) self.ordered_content_set.save() + # self.ordered_content_set.save_revision().publish() - self.ordered_content_set_timed = OrderedContentSet(name="Test set") + self.ordered_content_set_timed = OrderedContentSet(name="Test set timed") self.ordered_content_set_timed.pages.append( ( "pages", @@ -610,24 +612,25 @@ def test_orderedcontent_detail_endpoint_tags_flag(self, uclient): } assert content["pages"][0]["tags"] == [t.name for t in self.page1.tags.all()] - def test_orderedcontent_endpoint_with_drafts(self, uclient): - """ - Unpublished ordered content sets are returned if the qa param is set. - """ - self.ordered_content_set.unpublish() - url = "/api/v2/orderedcontent/?qa=True" - # it should return a list of ordered content sets with the unpublished one included - response = uclient.get(url) - content = json.loads(response.content) - - # the content set is not live but content is returned - assert not self.ordered_content_set.live - assert content["count"] == 2 - assert content["results"][0]["name"] == self.ordered_content_set.name - assert content["results"][0]["profile_fields"][0] == { - "profile_field": "gender", - "value": "female", - } + # TODO: Add this when we add support for qa param + # def test_orderedcontent_endpoint_with_drafts(self, uclient): + # """ + # Unpublished ordered content sets are returned if the qa param is set. + # """ + # self.ordered_content_set.unpublish() + # url = "/api/v2/orderedcontent/?qa=True" + # # it should return a list of ordered content sets with the unpublished one included + # response = uclient.get(url) + # content = json.loads(response.content) + + # # the content set is not live but content is returned + # assert not self.ordered_content_set.live + # assert content["count"] == 2 + # assert content["results"][0]["name"] == self.ordered_content_set.name + # assert content["results"][0]["profile_fields"][0] == { + # "profile_field": "gender", + # "value": "female", + # } def test_orderedcontent_endpoint_without_drafts(self, uclient): """ @@ -641,30 +644,31 @@ def test_orderedcontent_endpoint_without_drafts(self, uclient): # the content set is not live but content is returned assert not self.ordered_content_set.live - assert content["count"] == 1 - assert content["results"][0]["name"] == self.ordered_content_set_timed.name + assert content["count"] == 2 #TODO: Change this when we add support for qa param + assert content["results"][0]["name"] == self.ordered_content_set.name assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", "value": "female", } - def test_orderedcontent_detail_endpoint_with_drafts(self, uclient): - """ - Unpublished ordered content sets are returned if the qa param is set. - """ - self.ordered_content_set.unpublish() - url = f"/api/v2/orderedcontent/{self.ordered_content_set.id}/?qa=True" - # it should return specific ordered content set that is in draft - response = uclient.get(url) - content = json.loads(response.content) - - # the content set is not live but content is returned - assert not self.ordered_content_set.live - assert content["name"] == self.ordered_content_set.name - assert content["profile_fields"][0] == { - "profile_field": "gender", - "value": "female", - } + # TODO: Add this when we add support for qa param + # def test_orderedcontent_detail_endpoint_with_drafts(self, uclient): + # """ + # Unpublished ordered content sets are returned if the qa param is set. + # """ + # self.ordered_content_set.unpublish() + # url = f"/api/v2/orderedcontent/{self.ordered_content_set.id}/?qa=True" + # # it should return specific ordered content set that is in draft + # response = uclient.get(url) + # content = json.loads(response.content) + + # # the content set is not live but content is returned + # assert not self.ordered_content_set.live + # assert content["name"] == self.ordered_content_set.name + # assert content["profile_fields"][0] == { + # "profile_field": "gender", + # "value": "female", + # } def test_orderedcontent_detail_endpoint_without_drafts(self, uclient): """ @@ -678,6 +682,41 @@ def test_orderedcontent_detail_endpoint_without_drafts(self, uclient): # it redirects :TODO is it possible to resolve the redirect? assert response.status_code == 301 + def test_orderedcontent_new_draft(self, uclient): + """ + New revisions are returned if the qa param is set + """ + self.ordered_content_set.profile_fields.append( + ("relationship", "in_a_relationship") + ) + self.ordered_content_set.save_revision() + + response = uclient.get("/api/v2/orderedcontent/") + content = json.loads(response.content) + + assert self.ordered_content_set.live + + assert content["count"] == 2 + assert len(content["results"][0]["profile_fields"]) == 1 + assert content["results"][0]["name"] == self.ordered_content_set.name + assert content["results"][0]["profile_fields"][0] == { + "profile_field": "gender", + "value": "female", + } + + # TODO: When the qa param is introduced + # response = uclient.get("/api/v2/orderedcontent/?qa=True") + # content = json.loads(response.content) + # assert len(content["results"][0]["profile_fields"]) == 2 + # assert content["results"][0]["profile_fields"][0] == { + # "profile_field": "gender", + # "value": "female", + # } + # assert content["results"][0]["profile_fields"][1] == { + # "profile_field": "relationship", + # "value": "in_a_relationship", + # } + @pytest.mark.django_db class TestContentPageAPI2: From 94d90786e12dc6050f577ed313f6f6cbdfd7f4ef Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Thu, 15 Feb 2024 14:15:59 +0200 Subject: [PATCH 08/21] fix lint --- home/api.py | 5 ++--- home/tests/test_api.py | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/home/api.py b/home/api.py index a2617161..191fce9d 100644 --- a/home/api.py +++ b/home/api.py @@ -1,5 +1,4 @@ -from django.db.models.manager import BaseManager from rest_framework.exceptions import ValidationError from rest_framework.filters import SearchFilter from rest_framework.pagination import PageNumberPagination @@ -155,8 +154,8 @@ class OrderedContentSetViewSet(BaseAPIViewSet): filter_backends = (SearchFilter,) def get_queryset(self): - #TODO: Filter using the qa Param - #qa = self.request.query_params.get("qa") + # TODO: Filter using the qa Param + # qa = self.request.query_params.get("qa") queryset = OrderedContentSet.objects.all() return queryset diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 95bfa1f6..302b994a 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -644,7 +644,9 @@ def test_orderedcontent_endpoint_without_drafts(self, uclient): # the content set is not live but content is returned assert not self.ordered_content_set.live - assert content["count"] == 2 #TODO: Change this when we add support for qa param + assert ( + content["count"] == 2 + ) # TODO: Change this when we add support for qa param assert content["results"][0]["name"] == self.ordered_content_set.name assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", From c7c61c7dc00235bf858ab1a2703c9fd0aa272afa Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Thu, 15 Feb 2024 14:30:39 +0200 Subject: [PATCH 09/21] Fix tests --- home/api.py | 1 - home/tests/test_api.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/home/api.py b/home/api.py index 191fce9d..e1a8959e 100644 --- a/home/api.py +++ b/home/api.py @@ -1,4 +1,3 @@ - from rest_framework.exceptions import ValidationError from rest_framework.filters import SearchFilter from rest_framework.pagination import PageNumberPagination diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 302b994a..26b7d31e 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -488,7 +488,7 @@ def create_test_data(self): self.ordered_content_set.save() # self.ordered_content_set.save_revision().publish() - self.ordered_content_set_timed = OrderedContentSet(name="Test set timed") + self.ordered_content_set_timed = OrderedContentSet(name="Test set") self.ordered_content_set_timed.pages.append( ( "pages", @@ -647,7 +647,7 @@ def test_orderedcontent_endpoint_without_drafts(self, uclient): assert ( content["count"] == 2 ) # TODO: Change this when we add support for qa param - assert content["results"][0]["name"] == self.ordered_content_set.name + assert content["results"][0]["name"] == self.ordered_content_set_timed.name assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", "value": "female", @@ -700,7 +700,7 @@ def test_orderedcontent_new_draft(self, uclient): assert content["count"] == 2 assert len(content["results"][0]["profile_fields"]) == 1 - assert content["results"][0]["name"] == self.ordered_content_set.name + assert content["results"][0]["name"] == self.ordered_content_set_timed.name assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", "value": "female", From 0da50b939a2bd667ed22fcf298037cf232c35412 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Mon, 19 Feb 2024 09:57:15 +0200 Subject: [PATCH 10/21] Add qa param --- home/api.py | 15 +++++-- home/tests/test_api.py | 97 ++++++++++++++++++++---------------------- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/home/api.py b/home/api.py index e1a8959e..3a9fa743 100644 --- a/home/api.py +++ b/home/api.py @@ -153,10 +153,19 @@ class OrderedContentSetViewSet(BaseAPIViewSet): filter_backends = (SearchFilter,) def get_queryset(self): - # TODO: Filter using the qa Param - # qa = self.request.query_params.get("qa") + qa = self.request.query_params.get("qa") - queryset = OrderedContentSet.objects.all() + if qa: + # return the latest revision for each OrderedContentSet + queryset = OrderedContentSet.objects.all() + for ocs in queryset: + latest_revision = ocs.revisions.order_by("-created_at").first() + if latest_revision: + latest_revision = latest_revision.as_object() + ocs.profile_fields = latest_revision.profile_fields + + else: + queryset = OrderedContentSet.objects.all() return queryset diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 26b7d31e..e64f97c0 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -612,25 +612,24 @@ def test_orderedcontent_detail_endpoint_tags_flag(self, uclient): } assert content["pages"][0]["tags"] == [t.name for t in self.page1.tags.all()] - # TODO: Add this when we add support for qa param - # def test_orderedcontent_endpoint_with_drafts(self, uclient): - # """ - # Unpublished ordered content sets are returned if the qa param is set. - # """ - # self.ordered_content_set.unpublish() - # url = "/api/v2/orderedcontent/?qa=True" - # # it should return a list of ordered content sets with the unpublished one included - # response = uclient.get(url) - # content = json.loads(response.content) - - # # the content set is not live but content is returned - # assert not self.ordered_content_set.live - # assert content["count"] == 2 - # assert content["results"][0]["name"] == self.ordered_content_set.name - # assert content["results"][0]["profile_fields"][0] == { - # "profile_field": "gender", - # "value": "female", - # } + def test_orderedcontent_endpoint_with_drafts(self, uclient): + """ + Unpublished ordered content sets are returned if the qa param is set. + """ + self.ordered_content_set.unpublish() + url = "/api/v2/orderedcontent/?qa=True" + # it should return a list of ordered content sets with the unpublished one included + response = uclient.get(url) + content = json.loads(response.content) + + # the content set is not live but content is returned + assert not self.ordered_content_set.live + assert content["count"] == 2 + assert content["results"][0]["name"] == self.ordered_content_set.name + assert content["results"][0]["profile_fields"][0] == { + "profile_field": "gender", + "value": "female", + } def test_orderedcontent_endpoint_without_drafts(self, uclient): """ @@ -646,31 +645,30 @@ def test_orderedcontent_endpoint_without_drafts(self, uclient): assert not self.ordered_content_set.live assert ( content["count"] == 2 - ) # TODO: Change this when we add support for qa param + ) assert content["results"][0]["name"] == self.ordered_content_set_timed.name assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", "value": "female", } - # TODO: Add this when we add support for qa param - # def test_orderedcontent_detail_endpoint_with_drafts(self, uclient): - # """ - # Unpublished ordered content sets are returned if the qa param is set. - # """ - # self.ordered_content_set.unpublish() - # url = f"/api/v2/orderedcontent/{self.ordered_content_set.id}/?qa=True" - # # it should return specific ordered content set that is in draft - # response = uclient.get(url) - # content = json.loads(response.content) - - # # the content set is not live but content is returned - # assert not self.ordered_content_set.live - # assert content["name"] == self.ordered_content_set.name - # assert content["profile_fields"][0] == { - # "profile_field": "gender", - # "value": "female", - # } + def test_orderedcontent_detail_endpoint_with_drafts(self, uclient): + """ + Unpublished ordered content sets are returned if the qa param is set. + """ + self.ordered_content_set.unpublish() + url = f"/api/v2/orderedcontent/{self.ordered_content_set.id}/?qa=True" + # it should return specific ordered content set that is in draft + response = uclient.get(url) + content = json.loads(response.content) + + # the content set is not live but content is returned + assert not self.ordered_content_set.live + assert content["name"] == self.ordered_content_set.name + assert content["profile_fields"][0] == { + "profile_field": "gender", + "value": "female", + } def test_orderedcontent_detail_endpoint_without_drafts(self, uclient): """ @@ -706,18 +704,17 @@ def test_orderedcontent_new_draft(self, uclient): "value": "female", } - # TODO: When the qa param is introduced - # response = uclient.get("/api/v2/orderedcontent/?qa=True") - # content = json.loads(response.content) - # assert len(content["results"][0]["profile_fields"]) == 2 - # assert content["results"][0]["profile_fields"][0] == { - # "profile_field": "gender", - # "value": "female", - # } - # assert content["results"][0]["profile_fields"][1] == { - # "profile_field": "relationship", - # "value": "in_a_relationship", - # } + response = uclient.get("/api/v2/orderedcontent/?qa=True") + content = json.loads(response.content) + assert len(content["results"][0]["profile_fields"]) == 2 + assert content["results"][0]["profile_fields"][0] == { + "profile_field": "gender", + "value": "female", + } + assert content["results"][0]["profile_fields"][1] == { + "profile_field": "relationship", + "value": "in_a_relationship", + } @pytest.mark.django_db From 0e5ee36062be35551b6690c29dc062c89b297619 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Mon, 19 Feb 2024 10:03:33 +0200 Subject: [PATCH 11/21] Format --- home/tests/test_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/home/tests/test_api.py b/home/tests/test_api.py index e64f97c0..0fdf88c7 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -643,9 +643,7 @@ def test_orderedcontent_endpoint_without_drafts(self, uclient): # the content set is not live but content is returned assert not self.ordered_content_set.live - assert ( - content["count"] == 2 - ) + assert content["count"] == 2 assert content["results"][0]["name"] == self.ordered_content_set_timed.name assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", From f47294f3feb7b07c0f4e553d5283a369d5289988 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Wed, 21 Feb 2024 10:38:55 +0200 Subject: [PATCH 12/21] Update API to only return live items when qa param isn't set --- home/api.py | 2 +- home/tests/test_api.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/home/api.py b/home/api.py index 3a9fa743..3ce976d8 100644 --- a/home/api.py +++ b/home/api.py @@ -165,7 +165,7 @@ def get_queryset(self): ocs.profile_fields = latest_revision.profile_fields else: - queryset = OrderedContentSet.objects.all() + queryset = OrderedContentSet.objects.filter(live=True).order_by("last_published_at") return queryset diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 0fdf88c7..9e4cea1a 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -481,14 +481,12 @@ def create_test_data(self): with path.open(mode="rb") as f: import_content(f, "CSV", queue.Queue()) self.page1 = ContentPage.objects.first() - # self.page1.save_revision().publish() self.ordered_content_set = OrderedContentSet(name="Test set") self.ordered_content_set.pages.append(("pages", {"contentpage": self.page1})) self.ordered_content_set.profile_fields.append(("gender", "female")) self.ordered_content_set.save() - # self.ordered_content_set.save_revision().publish() - self.ordered_content_set_timed = OrderedContentSet(name="Test set") + self.ordered_content_set_timed = OrderedContentSet(name="Test set timed") self.ordered_content_set_timed.pages.append( ( "pages", @@ -643,7 +641,7 @@ def test_orderedcontent_endpoint_without_drafts(self, uclient): # the content set is not live but content is returned assert not self.ordered_content_set.live - assert content["count"] == 2 + assert content["count"] == 1 assert content["results"][0]["name"] == self.ordered_content_set_timed.name assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", @@ -696,14 +694,20 @@ def test_orderedcontent_new_draft(self, uclient): assert content["count"] == 2 assert len(content["results"][0]["profile_fields"]) == 1 - assert content["results"][0]["name"] == self.ordered_content_set_timed.name + assert content["results"][0]["name"] == self.ordered_content_set.name assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", "value": "female", } + assert content["results"][1]["name"] == self.ordered_content_set_timed.name + assert content["results"][1]["profile_fields"][0] == { + "profile_field": "gender", + "value": "female", + } response = uclient.get("/api/v2/orderedcontent/?qa=True") content = json.loads(response.content) + assert content["count"] == 2 assert len(content["results"][0]["profile_fields"]) == 2 assert content["results"][0]["profile_fields"][0] == { "profile_field": "gender", From edb1e4fbbb0398894c14fdd8e764ab5553baab0b Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Wed, 21 Feb 2024 16:03:38 +0200 Subject: [PATCH 13/21] Add pages to ordered content set api --- home/api.py | 3 +++ home/serializers.py | 4 ++-- home/tests/test_api.py | 31 ++++++++++++++++++++++++++----- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/home/api.py b/home/api.py index 3ce976d8..da8dc3f5 100644 --- a/home/api.py +++ b/home/api.py @@ -146,6 +146,7 @@ class OrderedContentSetViewSet(BaseAPIViewSet): listing_default_fields = BaseAPIViewSet.listing_default_fields + [ "name", "profile_fields", + "pages", ] known_query_parameters = BaseAPIViewSet.known_query_parameters.union(["page", "qa"]) pagination_class = PageNumberPagination @@ -162,6 +163,8 @@ def get_queryset(self): latest_revision = ocs.revisions.order_by("-created_at").first() if latest_revision: latest_revision = latest_revision.as_object() + ocs.name = latest_revision.name + ocs.pages = latest_revision.pages ocs.profile_fields = latest_revision.profile_fields else: diff --git a/home/serializers.py b/home/serializers.py index 94be8565..5b7e4056 100644 --- a/home/serializers.py +++ b/home/serializers.py @@ -399,7 +399,7 @@ def to_representation(self, instance): pages = [] for member in instance.pages: page = self.get_page_as_content_page(member.value.get("contentpage")) - title = page.title + title = page.title if page else "" if "whatsapp" in request.GET and page.enable_whatsapp is True: if page.whatsapp_title: title = page.whatsapp_title @@ -416,7 +416,7 @@ def to_representation(self, instance): if page.viber_title: title = page.viber_title page_data = { - "id": page.id, + "id": page.id if page else "", "title": title, "time": member.value.get("time"), "unit": member.value.get("unit"), diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 05cc5f31..08090d06 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -703,25 +703,38 @@ def test_orderedcontent_detail_endpoint_with_drafts(self, uclient): "value": "female", } - def test_orderedcontent_detail_endpoint_without_drafts(self, uclient): + def test_orderedcontent_detail_endpoint_without_drafts(self, uclient, settings): """ Unpublished ordered content sets are not returned if the qa param is not set. """ + settings.STATIC_ROOT = Path("home/tests/test_static") self.ordered_content_set.unpublish() url = f"/api/v2/orderedcontent/{self.ordered_content_set.id}" - # it should return nothing - response = uclient.get(url) - # it redirects :TODO is it possible to resolve the redirect? - assert response.status_code == 301 + response = uclient.get(url, follow=True) + + assert response.status_code == 404 def test_orderedcontent_new_draft(self, uclient): """ New revisions are returned if the qa param is set """ + self.ordered_content_set.pages.append( + ( + "pages", + { + "contentpage": self.page1, + "time": 2, + "unit": "Hours", + "before_or_after": "After", + "contact_field": "something", + }, + ) + ) self.ordered_content_set.profile_fields.append( ("relationship", "in_a_relationship") ) + self.ordered_content_set.save_revision() response = uclient.get("/api/v2/orderedcontent/") @@ -754,6 +767,14 @@ def test_orderedcontent_new_draft(self, uclient): "profile_field": "relationship", "value": "in_a_relationship", } + assert content["results"][0]["pages"][1] == { + "id": self.page1.id, + "title": self.page1.title, + "time": 2, + "unit": "Hours", + "before_or_after": "After", + "contact_field": "something", + } @pytest.mark.django_db From 24b20abd7e786da90fa556239c5ed6a37af2fab7 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Wed, 21 Feb 2024 16:15:32 +0200 Subject: [PATCH 14/21] Format --- home/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/home/api.py b/home/api.py index da8dc3f5..fc3feb2b 100644 --- a/home/api.py +++ b/home/api.py @@ -168,7 +168,9 @@ def get_queryset(self): ocs.profile_fields = latest_revision.profile_fields else: - queryset = OrderedContentSet.objects.filter(live=True).order_by("last_published_at") + queryset = OrderedContentSet.objects.filter(live=True).order_by( + "last_published_at" + ) return queryset From bce7afedd07163e0c43202e47e1e884b8ac358f2 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Wed, 21 Feb 2024 16:33:08 +0200 Subject: [PATCH 15/21] sort --- home/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/home/api.py b/home/api.py index fc3feb2b..198c40e7 100644 --- a/home/api.py +++ b/home/api.py @@ -158,7 +158,7 @@ def get_queryset(self): if qa: # return the latest revision for each OrderedContentSet - queryset = OrderedContentSet.objects.all() + queryset = OrderedContentSet.objects.all().order_by("last_published_at") for ocs in queryset: latest_revision = ocs.revisions.order_by("-created_at").first() if latest_revision: From decfd88f1db3a7ce95efccdb71c772bf3d1fa687 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Thu, 22 Feb 2024 10:56:44 +0200 Subject: [PATCH 16/21] Sort OCS --- home/api.py | 2 +- home/tests/test_api.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/home/api.py b/home/api.py index 198c40e7..1a39dc56 100644 --- a/home/api.py +++ b/home/api.py @@ -158,7 +158,7 @@ def get_queryset(self): if qa: # return the latest revision for each OrderedContentSet - queryset = OrderedContentSet.objects.all().order_by("last_published_at") + queryset = OrderedContentSet.objects.all().order_by("latest_revision_id") for ocs in queryset: latest_revision = ocs.revisions.order_by("-created_at").first() if latest_revision: diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 08090d06..2c3774fb 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -522,6 +522,7 @@ def create_test_data(self): self.ordered_content_set.pages.append(("pages", {"contentpage": self.page1})) self.ordered_content_set.profile_fields.append(("gender", "female")) self.ordered_content_set.save() + self.ordered_content_set.save_revision().publish() self.ordered_content_set_timed = OrderedContentSet(name="Test set timed") self.ordered_content_set_timed.pages.append( @@ -539,6 +540,7 @@ def create_test_data(self): self.ordered_content_set_timed.profile_fields.append(("gender", "female")) self.ordered_content_set_timed.save() + self.ordered_content_set_timed.save_revision().publish() def test_orderedcontent_endpoint(self, uclient): """ From 999c3fe959d6759f4c7a69bad66912ab13ad3be0 Mon Sep 17 00:00:00 2001 From: Gerrit Vermeulen Date: Thu, 22 Feb 2024 11:35:40 +0200 Subject: [PATCH 17/21] Fix test --- home/tests/test_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 2c3774fb..9cd359e7 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -760,16 +760,16 @@ def test_orderedcontent_new_draft(self, uclient): response = uclient.get("/api/v2/orderedcontent/?qa=True") content = json.loads(response.content) assert content["count"] == 2 - assert len(content["results"][0]["profile_fields"]) == 2 - assert content["results"][0]["profile_fields"][0] == { + assert len(content["results"][1]["profile_fields"]) == 2 + assert content["results"][1]["profile_fields"][0] == { "profile_field": "gender", "value": "female", } - assert content["results"][0]["profile_fields"][1] == { + assert content["results"][1]["profile_fields"][1] == { "profile_field": "relationship", "value": "in_a_relationship", } - assert content["results"][0]["pages"][1] == { + assert content["results"][1]["pages"][1] == { "id": self.page1.id, "title": self.page1.title, "time": 2, From 38e24546a7d6710cde33da97e4bccbbab7785166 Mon Sep 17 00:00:00 2001 From: Erik Harding Date: Thu, 22 Feb 2024 16:59:42 +0200 Subject: [PATCH 18/21] Replace create_test_data fixture on TestContentPageAPI --- home/tests/test_api.py | 231 ++++++++++++++++++----------------------- 1 file changed, 100 insertions(+), 131 deletions(-) diff --git a/home/tests/test_api.py b/home/tests/test_api.py index dbbde83e..3c6a2265 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -49,63 +49,41 @@ def uclient(client, django_user_model): @pytest.mark.django_db class TestContentPageAPI: - @pytest.fixture(autouse=True) - def create_test_data(self): - """ - Create the content that all the tests in this class will use. - """ - home_page = HomePage.objects.first() - main_menu = PageBuilder.build_cpi(home_page, "main-menu", "Main Menu") - content_page1 = PageBuilder.build_cp( - parent=main_menu, - slug="main-menu-first-time-user", - title="main menu first time user", - bodies=[ - WABody( - "main menu first time user", [WABlk("*Welcome to HealthAlert* 🌍")] - ), - MBody( - "main menu first time user", [MBlk("*Welcome to HealthAlert* 🌍")] - ), - SBody("main menu first time user", [SBlk("*Welcome to HealthAlert*")]), - UBody("main menu first time user", [UBlk("*Welcome to HealthAlert*")]), - ], - tags=["menu"], - quick_replies=["Self-help", "Settings", "Health Info"], - triggers=["Main menu"], - ) - PageBuilder.build_cp( - parent=content_page1, - slug="health-info", - title="health info", - bodies=[ - WABody("health info", [WABlk("*Health information* 🏥")]), - MBody("health info", [MBlk("*Health information* 🏥")]), - SBody("health info", [SBlk("*Health information* ")]), - UBody("health info", [UBlk("*Health information* ")]), - ], - tags=["health_info"], - ) - PageBuilder.build_cp( - parent=content_page1, - slug="self-help", - title="self-help", - bodies=[ - WABody("self-help", [WABlk("*Self-help programs* 🌬️")]), - MBody("self-help", [MBlk("*Self-help programs* 🌬️")]), - SBody("self-help", [SBlk("*Self-help programs*")]), - UBody("self-help", [UBlk("*Self-help programs*")]), - ], - tags=["self_help"], + def create_content_page( + self, parent=None, title="default page", tags=[], wa_body_count=1 + ): + if not parent: + home_page = HomePage.objects.first() + main_menu = PageBuilder.build_cpi(home_page, "main-menu", "Main Menu") + parent = main_menu + + bodies = [ + MBody(title, [MBlk("*Default Messenger Content* 🏥")]), + SBody(title, [SBlk("*Default SMS Content* ")]), + UBody(title, [UBlk("*Default USSD Content* ")]), + ] + + for i in range(wa_body_count): + wa_body = f"*Default WhatsApp Content {i+1}* 🏥" + bodies.append(WABody(title, [WABlk(wa_body)])) + + content_page = PageBuilder.build_cp( + parent=parent, + slug=title.replace(" ", "-"), + title=title, + bodies=bodies, + tags=tags, + quick_replies=[], + triggers=[], ) + return content_page def test_import_button_text(self, admin_client): """ Test that the import button on picker button template has the correct text """ - page = ContentPage.objects.first() - page_id = page.id - url = f"/admin/pages/{page_id}/edit/" + page = self.create_content_page() + url = f"/admin/pages/{page.id}/edit/" response = admin_client.get(url) assert response.status_code == 200 @@ -137,29 +115,39 @@ def test_login_required(self, client): def test_tag_filtering(self, uclient): """ If a tag filter is provided, only pages with matching tags are returned. + + FIXME: + * We should probably split this one too """ - # it should return 1 page for correct tag + page = self.create_content_page(tags=["menu"]) + self.create_content_page(page, title="Content Page 1") + self.create_content_page(page, title="Content Page 2") + unpublished_page = self.create_content_page( + page, title="Unpublished Page", tags=["Menu"] + ) + unpublished_page.unpublish() + + # it should return 1 page for correct tag, excluding unpublished pages with the + # same tag response = uclient.get("/api/v2/pages/?tag=menu") content = json.loads(response.content) assert content["count"] == 1 + # it should return 1 page for Uppercase tag response = uclient.get("/api/v2/pages/?tag=Menu") content = json.loads(response.content) assert content["count"] == 1 + # it should not return any pages for bogus tag response = uclient.get("/api/v2/pages/?tag=bogus") content = json.loads(response.content) assert content["count"] == 0 - # it should return all pages for no tag + + # it should return all pages for no tag, excluding home pages and index pages response = uclient.get("/api/v2/pages/") content = json.loads(response.content) - # exclude home pages and index pages assert content["count"] == 3 - # it should not return pages with tags in the draft - create_page(tags=["Menu"]).unpublish() - response = uclient.get("/api/v2/pages/?tag=Menu") - content = json.loads(response.content) - assert content["count"] == 1 + # If QA flag is sent then it should return pages with tags in the draft response = uclient.get("/api/v2/pages/?tag=Menu&qa=True") content = json.loads(response.content) @@ -169,41 +157,41 @@ def test_whatsapp_draft(self, uclient): """ Unpublished whatsapp pages are returned if the qa param is set. """ - page2 = ContentPage.objects.last() - page2.unpublish() - page_id = page2.id - url = f"/api/v2/pages/{page_id}/?whatsapp=True&qa=True" + page = self.create_content_page() + page.unpublish() + + url = f"/api/v2/pages/{page.id}/?whatsapp=True&qa=True" # it should return specific page that is in draft response = uclient.get(url) - content = json.loads(response.content) - message = "*Self-help programs* 🌬️" + content = response.json() + # the page is not live but whatsapp content is returned - assert not page2.live - assert content["body"]["text"]["value"]["message"].replace("\r", "") == message + assert not page.live + body = content["body"]["text"]["value"]["message"].replace("\r", "") + assert body == "*Default WhatsApp Content 1* 🏥" def test_messenger_draft(self, uclient): """ Unpublished messenger pages are returned if the qa param is set. """ - page2 = ContentPage.objects.last() - page2.unpublish() - page_id = page2.id - url = f"/api/v2/pages/{page_id}/?messenger=True&qa=True" + page = self.create_content_page() + page.unpublish() + + url = f"/api/v2/pages/{page.id}/?messenger=True&qa=True" # it should return specific page that is in draft response = uclient.get(url) - - message = "*Self-help programs* 🌬️" - content = json.loads(response.content) + content = response.json() # the page is not live but messenger content is returned - assert not page2.live - assert content["body"]["text"]["message"].replace("\r", "") == message + assert not page.live + body = content["body"]["text"]["message"].replace("\r", "") + assert body == "*Default Messenger Content* 🏥" def test_whatsapp_disabled(self, uclient): """ It should not return the web body if enable_whatsapp=false """ - page = ContentPage.objects.first() + page = self.create_content_page() page.enable_whatsapp = False page.save_revision().publish() response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=True") @@ -213,23 +201,7 @@ def test_message_number_specified(self, uclient): """ It should only return the 11th paragraph if 11th message is requested """ - page = ContentPage.objects.first() - - body = [] - for i in range(15): - block = blocks.StructBlock( - [ - ("message", blocks.TextBlock()), - ("variation_messages", blocks.ListBlock(VariationBlock())), - ] - ) - block_value = block.to_python( - {"message": f"WA Message {i+1}", "variation_messages": []} - ) - body.append(("Whatsapp_Message", block_value)) - - page.whatsapp_body = body - page.save_revision().publish() + page = self.create_content_page(wa_body_count=15) response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=True&message=11") content = json.loads(response.content) @@ -237,13 +209,14 @@ def test_message_number_specified(self, uclient): assert content["body"]["message"] == 11 assert content["body"]["next_message"] == 12 assert content["body"]["previous_message"] == 10 - assert content["body"]["text"]["value"]["message"] == "WA Message 11" + body = content["body"]["text"]["value"]["message"] + assert body == "*Default WhatsApp Content 11* 🏥" def test_no_message_number_specified(self, uclient): """ It should only return the first paragraph if no specific message is requested """ - page = ContentPage.objects.first() + page = self.create_content_page() response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=True") content = response.json() @@ -251,13 +224,14 @@ def test_no_message_number_specified(self, uclient): assert content["body"]["previous_message"] is None assert content["body"]["total_messages"] == 1 assert content["body"]["revision"] == page.get_latest_revision().id - assert "*Welcome to HealthAlert*" in content["body"]["text"]["value"]["message"] + body = content["body"]["text"]["value"]["message"] + assert body == "*Default WhatsApp Content 1* 🏥" def test_message_number_requested_out_of_range(self, uclient): """ It should return an appropriate error if requested message index is out of range """ - page = ContentPage.objects.first() + page = self.create_content_page() response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=True&message=3") assert response.status_code == 400 @@ -268,7 +242,7 @@ def test_message_number_requested_invalid(self, uclient): It should return an appropriate error if requested message is not a positive integer value """ - page = ContentPage.objects.first() + page = self.create_content_page() response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=True&message=notint") assert response.status_code == 400 @@ -285,6 +259,8 @@ def test_number_of_queries(self, uclient, django_assert_num_queries): """ # Run this once without counting, because there are two queries at the # end that only happen if this is the first test that runs. + page = self.create_content_page() + page = self.create_content_page(page, title="Content Page 1") uclient.get("/api/v2/pages/") with django_assert_num_queries(8): uclient.get("/api/v2/pages/") @@ -293,20 +269,20 @@ def test_detail_view_content(self, uclient): """ Fetching the detail view of a page returns the page content. """ - page2 = ContentPage.objects.last() - response = uclient.get(f"/api/v2/pages/{page2.id}/") + page = self.create_content_page(tags=["self_help"]) + response = uclient.get(f"/api/v2/pages/{page.id}/") content = response.json() # There's a lot of metadata, so only check selected fields. meta = content.pop("meta") assert meta["type"] == "home.ContentPage" - assert meta["slug"] == page2.slug - assert meta["parent"]["id"] == page2.get_parent().id + assert meta["slug"] == page.slug + assert meta["parent"]["id"] == page.get_parent().id assert meta["locale"] == "en" assert content == { - "id": page2.id, - "title": "self-help", + "id": page.id, + "title": "default page", "subtitle": "", "body": {"text": []}, "tags": ["self_help"], @@ -320,16 +296,16 @@ def test_detail_view_increments_count(self, uclient): """ Fetching the detail view of a page increments the view count. """ - page2 = ContentPage.objects.last() + page = self.create_content_page() assert PageView.objects.count() == 0 - uclient.get(f"/api/v2/pages/{page2.id}/") + uclient.get(f"/api/v2/pages/{page.id}/") assert PageView.objects.count() == 1 view = PageView.objects.last() assert view.message is None - uclient.get(f"/api/v2/pages/{page2.id}/") - uclient.get(f"/api/v2/pages/{page2.id}/") + uclient.get(f"/api/v2/pages/{page.id}/") + uclient.get(f"/api/v2/pages/{page.id}/") assert PageView.objects.count() == 3 view = PageView.objects.last() assert view.message is None @@ -339,47 +315,40 @@ def test_detail_view_with_children(self, uclient): Fetching the detail view of a page with children indicates that the page has children. """ - page1 = ContentPage.objects.first() - response = uclient.get(f"/api/v2/pages/{page1.id}/") + page = self.create_content_page() + self.create_content_page(page, title="Content Page 1") + self.create_content_page(page, title="Content Page 2") + + response = uclient.get(f"/api/v2/pages/{page.id}/") content = response.json() # There's a lot of metadata, so only check selected fields. meta = content.pop("meta") assert meta["type"] == "home.ContentPage" - assert meta["slug"] == page1.slug - assert meta["parent"]["id"] == page1.get_parent().id + assert meta["slug"] == page.slug + assert meta["parent"]["id"] == page.get_parent().id assert meta["locale"] == "en" - assert content == { - "id": page1.id, - "title": "main menu first time user", - "subtitle": "", - "body": {"text": []}, - "tags": ["menu"], - "triggers": ["Main menu"], - "quick_replies": ["Health Info", "Self-help", "Settings"], - "related_pages": [], - "has_children": True, - } + assert content["has_children"] is True def test_detail_view_whatsapp_message(self, uclient): """ Fetching a detail page and selecting the WhatsApp content returns the first WhatsApp message in the body. """ - page1 = ContentPage.objects.first() - response = uclient.get(f"/api/v2/pages/{page1.id}/?whatsapp=true") + page = self.create_content_page() + response = uclient.get(f"/api/v2/pages/{page.id}/?whatsapp=true") content = response.json() # There's a lot of metadata, so only check selected fields. meta = content.pop("meta") assert meta["type"] == "home.ContentPage" - assert meta["slug"] == page1.slug - assert meta["parent"]["id"] == page1.get_parent().id + assert meta["slug"] == page.slug + assert meta["parent"]["id"] == page.get_parent().id assert meta["locale"] == "en" - assert content["id"] == page1.id - assert content["title"] == "main menu first time user" + assert content["id"] == page.id + assert content["title"] == "default page" # There's a lot of body, so only check selected fields. body = content.pop("body") @@ -388,7 +357,7 @@ def test_detail_view_whatsapp_message(self, uclient): assert body["previous_message"] is None assert body["total_messages"] == 1 assert body["text"]["type"] == "Whatsapp_Message" - assert body["text"]["value"]["message"] == "*Welcome to HealthAlert* 🌍" + assert body["text"]["value"]["message"] == "*Default WhatsApp Content 1* 🏥" def test_detail_view_no_content_page(self, uclient): """ From f391bc7703042fe4162a323b5a1bb25a1f0ac459 Mon Sep 17 00:00:00 2001 From: Erik Harding Date: Thu, 22 Feb 2024 17:02:38 +0200 Subject: [PATCH 19/21] Remove pg 12 and 13 from test matrix --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc58e14f..7f3c335b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - postgres_version: ['12', '13', '14'] + postgres_version: ['14'] python-version: [3.10.6] redis-version: ['7.2'] services: From 04b7ca849a6035c9da3c4ca3825f7df624343253 Mon Sep 17 00:00:00 2001 From: Erik Harding Date: Thu, 22 Feb 2024 17:05:56 +0200 Subject: [PATCH 20/21] ruff ruff --- home/tests/test_api.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/home/tests/test_api.py b/home/tests/test_api.py index 3c6a2265..a703bf2d 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -5,7 +5,6 @@ import pytest from bs4 import BeautifulSoup from pytest_django.asserts import assertTemplateUsed -from wagtail import blocks from home.content_import_export import import_content from home.models import ( @@ -13,7 +12,6 @@ HomePage, OrderedContentSet, PageView, - VariationBlock, ) from .page_builder import ( @@ -50,7 +48,7 @@ def uclient(client, django_user_model): @pytest.mark.django_db class TestContentPageAPI: def create_content_page( - self, parent=None, title="default page", tags=[], wa_body_count=1 + self, parent=None, title="default page", tags=None, wa_body_count=1 ): if not parent: home_page = HomePage.objects.first() @@ -72,7 +70,7 @@ def create_content_page( slug=title.replace(" ", "-"), title=title, bodies=bodies, - tags=tags, + tags=tags or [], quick_replies=[], triggers=[], ) From 79ea1821afe7f096a7d00f5b1eb49f93f45bef51 Mon Sep 17 00:00:00 2001 From: Erik Harding Date: Mon, 26 Feb 2024 11:39:35 +0200 Subject: [PATCH 21/21] review fixes --- home/tests/page_builder.py | 5 ++++- home/tests/test_api.py | 42 +++++++++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/home/tests/page_builder.py b/home/tests/page_builder.py index b923136f..76db8890 100644 --- a/home/tests/page_builder.py +++ b/home/tests/page_builder.py @@ -226,6 +226,7 @@ def build_cp( whatsapp_template_name: str | None = None, whatsapp_template_category: str | None = None, translated_from: ContentPage | None = None, + publish: bool = True, ) -> ContentPage: builder = cls.cp(parent, slug, title).add_bodies(*bodies) if web_body: @@ -242,13 +243,15 @@ def build_cp( builder = builder.set_whatsapp_template_category(whatsapp_template_category) if translated_from: builder = builder.translated_from(translated_from) - return builder.build() + return builder.build(publish=publish) def build(self, publish: bool = True) -> TPage: self.parent.add_child(instance=self.page) rev = self.page.save_revision() if publish: rev.publish() + else: + self.page.unpublish() # The page instance is out of date after revision operations, so reload. self.page.refresh_from_db() return self.page diff --git a/home/tests/test_api.py b/home/tests/test_api.py index a703bf2d..3f6fe05b 100644 --- a/home/tests/test_api.py +++ b/home/tests/test_api.py @@ -48,8 +48,32 @@ def uclient(client, django_user_model): @pytest.mark.django_db class TestContentPageAPI: def create_content_page( - self, parent=None, title="default page", tags=None, wa_body_count=1 + self, + parent=None, + title="default page", + tags=None, + wa_body_count=1, + publish=True, ): + """ + Helper function to create pages needed for each test. + + Parameters + ---------- + parent : ContentPage + The ContentPage that will be used as the parent of the content page. + + If this is not provided, a ContentPageIndex object is created as a child of + the default home page and that is used as the parent. + title : str + Title of the content page. + tags : [str] + List of tags on the content page. + wa_body_count : int + How many WhatsApp message bodies to create on the content page. + publish: bool + Should the content page be published or not. + """ if not parent: home_page = HomePage.objects.first() main_menu = PageBuilder.build_cpi(home_page, "main-menu", "Main Menu") @@ -73,6 +97,7 @@ def create_content_page( tags=tags or [], quick_replies=[], triggers=[], + publish=publish, ) return content_page @@ -120,10 +145,9 @@ def test_tag_filtering(self, uclient): page = self.create_content_page(tags=["menu"]) self.create_content_page(page, title="Content Page 1") self.create_content_page(page, title="Content Page 2") - unpublished_page = self.create_content_page( - page, title="Unpublished Page", tags=["Menu"] + self.create_content_page( + page, title="Unpublished Page", tags=["Menu"], publish=False ) - unpublished_page.unpublish() # it should return 1 page for correct tag, excluding unpublished pages with the # same tag @@ -155,8 +179,7 @@ def test_whatsapp_draft(self, uclient): """ Unpublished whatsapp pages are returned if the qa param is set. """ - page = self.create_content_page() - page.unpublish() + page = self.create_content_page(publish=False) url = f"/api/v2/pages/{page.id}/?whatsapp=True&qa=True" # it should return specific page that is in draft @@ -165,15 +188,14 @@ def test_whatsapp_draft(self, uclient): # the page is not live but whatsapp content is returned assert not page.live - body = content["body"]["text"]["value"]["message"].replace("\r", "") + body = content["body"]["text"]["value"]["message"] assert body == "*Default WhatsApp Content 1* 🏥" def test_messenger_draft(self, uclient): """ Unpublished messenger pages are returned if the qa param is set. """ - page = self.create_content_page() - page.unpublish() + page = self.create_content_page(publish=False) url = f"/api/v2/pages/{page.id}/?messenger=True&qa=True" # it should return specific page that is in draft @@ -182,7 +204,7 @@ def test_messenger_draft(self, uclient): # the page is not live but messenger content is returned assert not page.live - body = content["body"]["text"]["message"].replace("\r", "") + body = content["body"]["text"]["message"] assert body == "*Default Messenger Content* 🏥" def test_whatsapp_disabled(self, uclient):