diff --git a/config/settings/base.py b/config/settings/base.py index d4005882c..5e4229c71 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -305,6 +305,8 @@ } } +WAGTAILDOCS_DOCUMENT_MODEL = "core.CustomDocument" + WAGTAILIMAGES_IMAGE_MODEL = "images.CustomImage" # Custom password template for private pages diff --git a/etna/core/blocks/__init__.py b/etna/core/blocks/__init__.py index c1593bfd2..fb850b811 100644 --- a/etna/core/blocks/__init__.py +++ b/etna/core/blocks/__init__.py @@ -1,5 +1,6 @@ from .base import SectionDepthAwareStructBlock from .cta import ButtonBlock, CallToActionBlock, LargeCardLinksBlock +from .document import DocumentsBlock from .featured_content import ( FeaturedCollectionBlock, FeaturedRecordArticleBlock, @@ -27,6 +28,7 @@ "ButtonBlock", "CallToActionBlock", "ContentImageBlock", + "DocumentsBlock", "DoDontListBlock", "FeaturedRecordArticleBlock", "FeaturedCollectionBlock", diff --git a/etna/core/blocks/document.py b/etna/core/blocks/document.py new file mode 100644 index 000000000..715519a38 --- /dev/null +++ b/etna/core/blocks/document.py @@ -0,0 +1,45 @@ +from wagtail import blocks +from wagtail.documents.blocks import DocumentChooserBlock + + +class DocumentBlock(blocks.StructBlock): + """ + A block for embedding a document file in a page. + """ + + file = DocumentChooserBlock(required=True) + + def get_api_representation(self, value, context=None): + representation = super().get_api_representation(value, context) + + file = value.get("file") + + if file: + representation["file"] = { + "id": file.id, + "title": file.title, + "description": file.description or None, + "file_size": file.file_size, + "pretty_file_size": file.pretty_file_size, + "type": file.file_extension, + "extent": file.extent, + "url": file.file.url, + } + + return representation + + class Meta: + icon = "doc-full" + label = "Document" + + +class DocumentsBlock(blocks.StructBlock): + """ + A block for embedding multiple document files in a page. + """ + + documents = blocks.ListBlock(DocumentBlock()) + + class Meta: + icon = "doc-full" + label = "Documents" diff --git a/etna/core/migrations/0004_customdocument.py b/etna/core/migrations/0004_customdocument.py new file mode 100644 index 000000000..439e7bc23 --- /dev/null +++ b/etna/core/migrations/0004_customdocument.py @@ -0,0 +1,88 @@ +# Generated by Django 5.0.6 on 2024-07-02 15:35 + +import django.db.models.deletion +import taggit.managers +import wagtail.models.media +import wagtail.search.index +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0003_set_page_uuid"), + ( + "taggit", + "0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx", + ), + ("wagtailcore", "0093_uploadedfile"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="CustomDocument", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=255, verbose_name="title")), + ("file", models.FileField(upload_to="documents", verbose_name="file")), + ( + "created_at", + models.DateTimeField(auto_now_add=True, verbose_name="created at"), + ), + ("file_size", models.PositiveIntegerField(editable=False, null=True)), + ( + "file_hash", + models.CharField(blank=True, editable=False, max_length=40), + ), + ("extent", models.CharField(blank=True, null=True)), + ("description", models.TextField(blank=True, null=True)), + ( + "collection", + models.ForeignKey( + default=wagtail.models.media.get_root_collection_id, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="wagtailcore.collection", + verbose_name="collection", + ), + ), + ( + "tags", + taggit.managers.TaggableManager( + blank=True, + help_text=None, + through="taggit.TaggedItem", + to="taggit.Tag", + verbose_name="tags", + ), + ), + ( + "uploaded_by_user", + models.ForeignKey( + blank=True, + editable=False, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + verbose_name="uploaded by user", + ), + ), + ], + options={ + "verbose_name": "document", + "verbose_name_plural": "documents", + "abstract": False, + }, + bases=(wagtail.search.index.Indexed, models.Model), + ), + ] diff --git a/etna/core/models/__init__.py b/etna/core/models/__init__.py index 0a370114f..24f5a8e63 100644 --- a/etna/core/models/__init__.py +++ b/etna/core/models/__init__.py @@ -1,4 +1,5 @@ from .basepage import * # noqa +from .documents import * # noqa from .forms import * # noqa from .mixins import * # noqa from .settings import * # noqa diff --git a/etna/core/models/documents.py b/etna/core/models/documents.py new file mode 100644 index 000000000..7d6aa382a --- /dev/null +++ b/etna/core/models/documents.py @@ -0,0 +1,27 @@ +import re + +from django.db import models + +from wagtail.documents.models import AbstractDocument, Document + + +class CustomDocument(AbstractDocument): + extent = models.CharField(blank=True, null=True) + description = models.TextField(blank=True, null=True) + + @property + def pretty_file_size(self): + suffixes = ["B", "kB", "MB", "GB"] + i = 0 + pretty_file_size = self.file_size + while pretty_file_size >= 1000 and i < len(suffixes) - 1: + pretty_file_size /= 1000 + i += 1 + return re.sub( + r"\.0+$", "", f"{pretty_file_size:.{max(i - 1, 0)}f}{suffixes[i]}" + ) + + admin_form_fields = Document.admin_form_fields + ( + "extent", + "description", + ) diff --git a/etna/generic_pages/blocks.py b/etna/generic_pages/blocks.py index cf52b139d..3c336bc06 100644 --- a/etna/generic_pages/blocks.py +++ b/etna/generic_pages/blocks.py @@ -4,6 +4,7 @@ ButtonBlock, CallToActionBlock, ContentImageBlock, + DocumentsBlock, DoDontListBlock, FeaturedRecordArticleBlock, InsetTextBlock, @@ -23,6 +24,7 @@ class SectionContentBlock(blocks.StreamBlock): button = ButtonBlock() call_to_action = CallToActionBlock() + document = DocumentsBlock() do_dont_list = DoDontListBlock() featured_record_article = FeaturedRecordArticleBlock() image = ContentImageBlock() diff --git a/etna/generic_pages/migrations/0017_alter_generalpage_body.py b/etna/generic_pages/migrations/0017_alter_generalpage_body.py new file mode 100644 index 000000000..331a0e127 --- /dev/null +++ b/etna/generic_pages/migrations/0017_alter_generalpage_body.py @@ -0,0 +1,592 @@ +# Generated by Django 5.0.6 on 2024-07-10 10:02 +# etna:allowAlterField + +import etna.core.blocks.image +import etna.core.blocks.page_chooser +import etna.core.blocks.paragraph +import etna.media.blocks +import etna.records.blocks +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +import wagtail.snippets.blocks +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("generic_pages", "0016_generalpage_intro"), + ] + + operations = [ + migrations.AlterField( + model_name="generalpage", + name="body", + field=wagtail.fields.StreamField( + [ + ( + "content_section", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + label="Heading", max_length=100 + ), + ), + ( + "content", + wagtail.blocks.StreamBlock( + [ + ( + "button", + wagtail.blocks.StructBlock( + [ + ( + "label", + wagtail.blocks.CharBlock(), + ), + ( + "link", + etna.core.blocks.page_chooser.APIPageChooserBlock( + required=False + ), + ), + ( + "external_link", + wagtail.blocks.URLBlock( + required=False + ), + ), + ( + "accented", + wagtail.blocks.BooleanBlock( + help_text="Use the accented button style", + label="Accented", + required=False, + ), + ), + ] + ), + ), + ( + "call_to_action", + wagtail.blocks.StructBlock( + [ + ( + "body", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + "ol", + "ul", + ], + max_length=100, + ), + ), + ( + "button", + wagtail.blocks.StructBlock( + [ + ( + "label", + wagtail.blocks.CharBlock(), + ), + ( + "link", + etna.core.blocks.page_chooser.APIPageChooserBlock( + required=False + ), + ), + ( + "external_link", + wagtail.blocks.URLBlock( + required=False + ), + ), + ( + "accented", + wagtail.blocks.BooleanBlock( + help_text="Use the accented button style", + label="Accented", + required=False, + ), + ), + ] + ), + ), + ] + ), + ), + ( + "document", + wagtail.blocks.StructBlock( + [ + ( + "documents", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "file", + wagtail.documents.blocks.DocumentChooserBlock( + required=True + ), + ) + ] + ) + ), + ) + ] + ), + ), + ( + "do_dont_list", + wagtail.blocks.StructBlock( + [ + ( + "do", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "text", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + ] + ), + ) + ], + icon="check", + label="Do item", + ), + label="Dos", + ), + ), + ( + "dont", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "text", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + ] + ), + ) + ], + icon="cross", + label="Don't item", + ), + label="Don'ts", + ), + ), + ] + ), + ), + ( + "featured_record_article", + wagtail.blocks.StructBlock( + [ + ( + "page", + etna.core.blocks.page_chooser.APIPageChooserBlock( + label="Page", + page_type=[ + "articles.RecordArticlePage" + ], + required_api_fields=[ + "teaser_image" + ], + ), + ) + ] + ), + ), + ( + "image", + wagtail.blocks.StructBlock( + [ + ( + "image", + etna.core.blocks.image.APIImageChooserBlock( + rendition_size="max-900x900", + required=True, + ), + ), + ( + "alt_text", + wagtail.blocks.CharBlock( + help_text='Alternative (alt) text describes images when they fail to load, and is read aloud by assistive technologies. Use a maximum of 100 characters to describe your image. Check the guidance for tips on writing alt text.', + label="Alternative text", + max_length=100, + ), + ), + ( + "caption", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + ], + help_text="If provided, displays directly below the image. Can be used to specify sources, transcripts or other useful metadata.", + label="Caption (optional)", + required=False, + ), + ), + ] + ), + ), + ( + "inset_text", + wagtail.blocks.StructBlock( + [ + ( + "text", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + "ol", + "ul", + ] + ), + ) + ] + ), + ), + ( + "media", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="A descriptive title for the media block", + required=True, + ), + ), + ( + "background_image", + etna.core.blocks.image.APIImageChooserBlock( + help_text="A background image for the media block" + ), + ), + ( + "media", + etna.media.blocks.MediaChooserBlock(), + ), + ] + ), + ), + ( + "paragraph", + wagtail.blocks.StructBlock( + [ + ( + "text", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + "ol", + "ul", + ] + ), + ) + ] + ), + ), + ( + "promoted_item", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Title of the promoted page", + label="Title", + max_length=100, + ), + ), + ( + "category", + wagtail.blocks.ChoiceBlock( + choices=[ + ( + "blog", + "Blog post", + ), + ( + "podcast", + "Podcast", + ), + ("video", "Video"), + ( + "video-external", + "External video", + ), + ( + "external-link", + "External link", + ), + ], + label="Category", + ), + ), + ( + "publication_date", + wagtail.blocks.CharBlock( + help_text="This is a free text field. Please enter date as per agreed format: 14 April 2021", + required=False, + ), + ), + ( + "author", + wagtail.blocks.CharBlock( + required=False + ), + ), + ( + "duration", + wagtail.blocks.CharBlock( + help_text="Podcast or video duration.", + label="Duration", + max_length=50, + required=False, + ), + ), + ( + "url", + wagtail.blocks.URLBlock( + help_text="URL for the external page", + label="External URL", + ), + ), + ( + "target_blank", + wagtail.blocks.BooleanBlock( + label="Should this URL open in a new tab?

Tick the box if 'yes'

", + required=False, + ), + ), + ( + "cta_label", + wagtail.blocks.CharBlock( + help_text="The text displayed on the button for your URL. If your URL links to an external site, please add the name of the site users will land on, and what they will find on this page. For example 'Watch our short film about Shakespeare on YouTube'.", + label="Call to action label", + max_length=50, + ), + ), + ( + "image", + wagtail.blocks.StructBlock( + [ + ( + "image", + etna.core.blocks.image.APIImageChooserBlock( + rendition_size="max-900x900", + required=True, + ), + ), + ( + "decorative", + wagtail.blocks.BooleanBlock( + default=False, + help_text='Decorative images are used for visual effect and do not add information to the content of a page. "Check the guidance to see if your image is decorative.', + label="Is this image decorative?

Tick the box if 'yes'

", + required=False, + ), + ), + ( + "alt_text", + wagtail.blocks.CharBlock( + help_text='Alternative (alt) text describes images when they fail to load, and is read aloud by assistive technologies. Use a maximum of 100 characters to describe your image. Decorative images do not require alt text. Check the guidance for tips on writing alt text.', + label="Image alternative text", + max_length=100, + required=False, + ), + ), + ], + label="Teaser image", + template="articles/blocks/images/blog-embed__image-container.html", + ), + ), + ( + "description", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + ], + help_text="A description of the promoted page", + ), + ), + ] + ), + ), + ( + "promoted_list", + wagtail.blocks.StructBlock( + [ + ( + "category", + wagtail.snippets.blocks.SnippetChooserBlock( + "categories.Category" + ), + ), + ( + "summary", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + ], + required=False, + ), + ), + ( + "promoted_items", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="The title of the target page", + max_length=100, + required=True, + ), + ), + ( + "description", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + ], + help_text="A description of the target page", + required=False, + ), + ), + ( + "url", + wagtail.blocks.URLBlock( + required=True + ), + ), + ] + ) + ), + ), + ] + ), + ), + ( + "quote", + wagtail.blocks.StructBlock( + [ + ( + "quote", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + ], + required=True, + ), + ), + ( + "attribution", + wagtail.blocks.CharBlock( + max_length=100, + required=False, + ), + ), + ] + ), + ), + ( + "record_links", + wagtail.blocks.StructBlock( + [ + ( + "items", + wagtail.blocks.ListBlock( + etna.records.blocks.RecordLinkBlock, + label="Items", + ), + ) + ] + ), + ), + ( + "sub_heading", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + label="Sub-heading", + max_length=100, + ), + ) + ] + ), + ), + ( + "warning_text", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + help_text="Optional heading for the warning text, for screen readers", + required=False, + ), + ), + ( + "body", + etna.core.blocks.paragraph.APIRichTextBlock( + features=[ + "bold", + "italic", + "link", + "ol", + "ul", + ] + ), + ), + ] + ), + ), + ], + required=False, + ), + ), + ] + ), + ) + ], + blank=True, + null=True, + ), + ), + ]