diff --git a/config/settings/base.py b/config/settings/base.py index 8a2e82427..1622e699a 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -50,6 +50,7 @@ "etna.analytics", "etna.articles", "etna.people", + "etna.cookies", "etna.categories", "etna.ciim", "etna.collections", diff --git a/etna/cookies/__init__.py b/etna/cookies/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/etna/cookies/apps.py b/etna/cookies/apps.py new file mode 100644 index 000000000..77f152582 --- /dev/null +++ b/etna/cookies/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class CookiesAppConfig(AppConfig): + default_auto_field = "django.db.models.AutoField" + name = "etna.cookies" + verbose_name = "Cookies" diff --git a/etna/cookies/blocks.py b/etna/cookies/blocks.py new file mode 100644 index 000000000..38a5811aa --- /dev/null +++ b/etna/cookies/blocks.py @@ -0,0 +1,36 @@ +from wagtail import blocks + +from etna.core.blocks import ( + ContentTableBlock, + DescriptionListBlock, + InsetTextBlock, + ParagraphBlock, + SectionDepthAwareStructBlock, + SubHeadingBlock, +) + + +class SectionContentBlock(blocks.StreamBlock): + inset_text = InsetTextBlock() + paragraph = ParagraphBlock() + description_list = DescriptionListBlock() + sub_heading = SubHeadingBlock() + table = ContentTableBlock() + + +class ContentSectionBlock(SectionDepthAwareStructBlock): + heading = blocks.CharBlock(max_length=100, label="Heading") + content = SectionContentBlock(required=False) + + class Meta: + label = "Section" + template = "articles/blocks/section.html" + + +class CookieDetailsStreamBlock(SectionContentBlock): + """ + A block for the CookieDetailsPage model. + """ + + content_section = ContentSectionBlock() + sub_heading = None diff --git a/etna/cookies/factories.py b/etna/cookies/factories.py new file mode 100644 index 000000000..5431e0b30 --- /dev/null +++ b/etna/cookies/factories.py @@ -0,0 +1,7 @@ +from etna.cookies import models as app_models +from etna.core.factories import BasePageFactory + + +class CookiesPageFactory(BasePageFactory): + class Meta: + model = app_models.CookiesPage diff --git a/etna/cookies/migrations/0001_initial.py b/etna/cookies/migrations/0001_initial.py new file mode 100644 index 000000000..e54c84b75 --- /dev/null +++ b/etna/cookies/migrations/0001_initial.py @@ -0,0 +1,339 @@ +# Generated by Django 5.0.8 on 2024-08-28 13:32 + +import django.db.models.deletion +import etna.analytics.mixins +import uuid +import wagtail.fields +import wagtail_headless_preview.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("alerts", "0003_alert_name_alter_alert_title"), + ("images", "0009_alter_customimage_custom_sensitive_image_warning"), + ("wagtailcore", "0094_alter_page_locale"), + ] + + operations = [ + migrations.CreateModel( + name="CookieDetailsPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "twitter_og_title", + models.CharField( + blank=True, + help_text="If left blank, the OpenGraph title will be used.", + max_length=255, + null=True, + verbose_name="Twitter OpenGraph title", + ), + ), + ( + "twitter_og_description", + models.TextField( + blank=True, + help_text="If left blank, the OpenGraph description will be used.", + null=True, + verbose_name="Twitter OpenGraph description", + ), + ), + ( + "teaser_text", + models.TextField( + help_text="A short, enticing description of this page. This will appear in promos and under thumbnails around the site.", + max_length=160, + verbose_name="teaser text", + ), + ), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + unique=True, + verbose_name="UUID", + ), + ), + ( + "intro", + wagtail.fields.RichTextField( + help_text="1-2 sentences introducing the subject of the page, and explaining why a user should read on.", + max_length=300, + verbose_name="introductory text", + ), + ), + ( + "body", + wagtail.fields.StreamField( + [ + ("inset_text", 1), + ("paragraph", 1), + ("description_list", 6), + ("table", 8), + ("content_section", 13), + ], + blank=True, + block_lookup={ + 0: ( + "etna.core.blocks.paragraph.APIRichTextBlock", + (), + {"features": ["bold", "italic", "link", "ol", "ul"]}, + ), + 1: ("wagtail.blocks.StructBlock", [[("text", 0)]], {}), + 2: ("wagtail.blocks.CharBlock", (), {"required": True}), + 3: ( + "etna.core.blocks.paragraph.APIRichTextBlock", + (), + {"features": ["bold", "italic", "link"]}, + ), + 4: ( + "wagtail.blocks.StructBlock", + [[("term", 2), ("detail", 3)]], + {}, + ), + 5: ("wagtail.blocks.ListBlock", (4,), {}), + 6: ("wagtail.blocks.StructBlock", [[("items", 5)]], {}), + 7: ( + "wagtail.contrib.table_block.blocks.TableBlock", + (), + { + "table_options": { + "contextMenu": [ + "row_above", + "row_below", + "---------", + "col_left", + "col_right", + "---------", + "remove_row", + "remove_col", + "---------", + "undo", + "redo", + "---------", + "alignment", + ] + } + }, + ), + 8: ("wagtail.blocks.StructBlock", [[("table", 7)]], {}), + 9: ( + "wagtail.blocks.CharBlock", + (), + {"label": "Heading", "max_length": 100}, + ), + 10: ( + "wagtail.blocks.CharBlock", + (), + {"label": "Sub-heading", "max_length": 100}, + ), + 11: ("wagtail.blocks.StructBlock", [[("heading", 10)]], {}), + 12: ( + "wagtail.blocks.StreamBlock", + [ + [ + ("inset_text", 1), + ("paragraph", 1), + ("description_list", 6), + ("sub_heading", 11), + ("table", 8), + ] + ], + {"required": False}, + ), + 13: ( + "wagtail.blocks.StructBlock", + [[("heading", 9), ("content", 12)]], + {}, + ), + }, + null=True, + ), + ), + ( + "alert", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="alerts.alert", + ), + ), + ( + "search_image", + models.ForeignKey( + blank=True, + help_text="Image that will appear when this page is shared on social media. This will default to the teaser image if left blank.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + verbose_name="OpenGraph image", + ), + ), + ( + "teaser_image", + models.ForeignKey( + blank=True, + help_text="Image that will appear on thumbnails and promos around the site.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + ), + ), + ( + "twitter_og_image", + models.ForeignKey( + blank=True, + help_text="If left blank, the OpenGraph image will be used.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + verbose_name="Twitter OpenGraph image", + ), + ), + ], + options={ + "verbose_name": "Cookie details page", + "verbose_name_plural": "Cookie details pages", + }, + bases=( + etna.analytics.mixins.DataLayerMixin, + wagtail_headless_preview.models.HeadlessPreviewMixin, + "wagtailcore.page", + models.Model, + ), + ), + migrations.CreateModel( + name="CookiesPage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "twitter_og_title", + models.CharField( + blank=True, + help_text="If left blank, the OpenGraph title will be used.", + max_length=255, + null=True, + verbose_name="Twitter OpenGraph title", + ), + ), + ( + "twitter_og_description", + models.TextField( + blank=True, + help_text="If left blank, the OpenGraph description will be used.", + null=True, + verbose_name="Twitter OpenGraph description", + ), + ), + ( + "teaser_text", + models.TextField( + help_text="A short, enticing description of this page. This will appear in promos and under thumbnails around the site.", + max_length=160, + verbose_name="teaser text", + ), + ), + ( + "uuid", + models.UUIDField( + default=uuid.uuid4, + editable=False, + unique=True, + verbose_name="UUID", + ), + ), + ( + "intro", + wagtail.fields.RichTextField( + help_text="1-2 sentences introducing the subject of the page, and explaining why a user should read on.", + max_length=300, + verbose_name="introductory text", + ), + ), + ( + "alert", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="alerts.alert", + ), + ), + ( + "search_image", + models.ForeignKey( + blank=True, + help_text="Image that will appear when this page is shared on social media. This will default to the teaser image if left blank.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + verbose_name="OpenGraph image", + ), + ), + ( + "teaser_image", + models.ForeignKey( + blank=True, + help_text="Image that will appear on thumbnails and promos around the site.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + ), + ), + ( + "twitter_og_image", + models.ForeignKey( + blank=True, + help_text="If left blank, the OpenGraph image will be used.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="images.customimage", + verbose_name="Twitter OpenGraph image", + ), + ), + ], + options={ + "abstract": False, + }, + bases=( + etna.analytics.mixins.DataLayerMixin, + wagtail_headless_preview.models.HeadlessPreviewMixin, + "wagtailcore.page", + models.Model, + ), + ), + ] diff --git a/etna/cookies/migrations/__init__.py b/etna/cookies/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/etna/cookies/models.py b/etna/cookies/models.py new file mode 100755 index 000000000..d15e1093c --- /dev/null +++ b/etna/cookies/models.py @@ -0,0 +1,31 @@ +from wagtail.admin.panels import FieldPanel +from wagtail.api import APIField +from wagtail.fields import StreamField + +from etna.core.models import BasePageWithRequiredIntro + +from .blocks import CookieDetailsStreamBlock + + +class CookiesPage(BasePageWithRequiredIntro): + max_count = 1 + subpage_types = ["cookies.CookieDetailsPage"] + + +class CookieDetailsPage(BasePageWithRequiredIntro): + max_count = 1 + parent_page_types = ["cookies.CookiesPage"] + subpage_types = [] + + body = StreamField(CookieDetailsStreamBlock, blank=True, null=True) + + content_panels = BasePageWithRequiredIntro.content_panels + [ + FieldPanel("body"), + ] + + class Meta: + verbose_name = "Cookie details page" + verbose_name_plural = "Cookie details pages" + verbose_name_public = "cookies" + + api_fields = BasePageWithRequiredIntro.api_fields + [APIField("body")]