From 46ece396b6bbe8f0b0e30d37dcdf82e4f1642056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <git@imaniti.org> Date: Wed, 3 Jan 2024 20:26:49 +0100 Subject: [PATCH] move further models to mixins --- main/blocks.py | 21 +- ...5_alter_mainhomepage_footer_other_links.py | 5 +- main/models.py | 316 +--------------- shared/blocks/main.py | 29 +- shared/const.py | 15 + shared/forms.py | 79 ++++ shared/models/main.py | 346 +++++++++++++++++- 7 files changed, 462 insertions(+), 349 deletions(-) diff --git a/main/blocks.py b/main/blocks.py index faac594d..f257d8f1 100644 --- a/main/blocks.py +++ b/main/blocks.py @@ -9,7 +9,6 @@ from wagtail.blocks import ( TextBlock, URLBlock, ) -from wagtail.documents.blocks import DocumentChooserBlock from wagtail.images.blocks import ImageChooserBlock from shared.blocks import CardLinkBlockMixin, CardLinkWithHeadlineBlockMixin, CTAMixin @@ -250,16 +249,6 @@ class PersonContactBoxBlock(StructBlock): # ARTICLE BLOCKS -class ArticleQuoteBlock(StructBlock): - quote = CharBlock(label="Citace") - autor_name = CharBlock(label="Jméno autora") - - class Meta: - icon = "user" - label = "Blok citace" - template = "main/includes/legacy/article_quote_block.html" - - class ArticleImageMixin(StructBlock): image = ImageChooserBlock(label="Obrázek") image_source = CharBlock( @@ -285,15 +274,6 @@ class ArticleRightImageBlock(ArticleImageMixin): template = "main/includes/molecules/articles/article_richtext_content_with_right_image.html" -class ArticleDownloadBlock(StructBlock): - file = DocumentChooserBlock(label="Stáhnutelný soubor") - - class Meta: - icon = "user" - label = "Blok stáhnutelného dokumentu" - template = "main/includes/molecules/blocks/article_download_block.html" - - class TwoTextColumnBlock(StructBlock): text_column_1 = RichTextBlock(label="První sloupec textu") text_column_2 = RichTextBlock(label="Druhý sloupec textu") @@ -352,5 +332,6 @@ class TeamBlock(StructBlock): # --- TODO: Remove legacy blocks used in migrations only + class LinkBlock(StructBlock): pass diff --git a/main/migrations/0065_alter_mainhomepage_footer_other_links.py b/main/migrations/0065_alter_mainhomepage_footer_other_links.py index 787c9193..2d06c8fb 100644 --- a/main/migrations/0065_alter_mainhomepage_footer_other_links.py +++ b/main/migrations/0065_alter_mainhomepage_footer_other_links.py @@ -1,9 +1,10 @@ # Generated by Django 4.1.10 on 2024-01-03 19:04 -from django.db import migrations -import shared.blocks.main import wagtail.blocks import wagtail.fields +from django.db import migrations + +import shared.blocks.main class Migration(migrations.Migration): diff --git a/main/models.py b/main/models.py index 65cb63fa..305ad9e1 100644 --- a/main/models.py +++ b/main/models.py @@ -29,6 +29,7 @@ from wagtail.search import index from wagtailmetadata.models import MetadataPageMixin from calendar_utils.models import CalendarMixin +from shared import blocks as shared_blocks from shared.forms import SubscribeForm from shared.models import ( # MenuMixin, ArticleMixin, @@ -36,22 +37,20 @@ from shared.models import ( # MenuMixin, ArticlesPageMixin, ExtendedMetadataHomePageMixin, ExtendedMetadataPageMixin, + MainArticlePageMixin, + MainArticlesPageMixin, + MainHomePageMixin, SharedTaggedMainArticle, SubpageMixin, ) from shared.utils import make_promote_panels, subscribe_to_newsletter -from shared import blocks as shared_blocks from tuning import admin_help from . import blocks -from .constants import MONTH_NAMES from .forms import JekyllImportForm from .menu import MenuMixin, PageInMenuMixin -from shared.models import MainHomePageMixin - - class MainHomePage(MainHomePageMixin): # content content = StreamField( @@ -128,251 +127,10 @@ class MainHomePage(MainHomePageMixin): return self.setup_article_page_context(request) -class MainArticlesPage( - RoutablePageMixin, - ExtendedMetadataPageMixin, - SubpageMixin, - MetadataPageMixin, - ArticlesPageMixin, - PageInMenuMixin, - Page, -): - last_import_log = models.TextField( - "Výstup z posledního importu", null=True, blank=True - ) - perex = models.TextField() - - import_panels = [ - MultiFieldPanel( - [ - FieldPanel("do_import"), - FieldPanel("collection"), - FieldPanel("dry_run"), - FieldPanel("jekyll_repo_url"), - FieldPanel("readonly_log"), - HelpPanel( - "Import provádějte vždy až po vytvoření stránky aktualit. " - 'Pro uložení logu je nutné volit možnost "Publikovat", nikoliv' - 'pouze "Uložit koncept". ' - "Import proběhne na pozadí a může trvat až několik minut. " - "Dejte si po spuštění importu kávu a potom obnovte stránku pro " - "zobrazení výsledku importu." - ), - ], - "import z Jekyll repozitáře", - ), - ] - - ### RELATIONS - +class MainArticlesPage(MainArticlesPageMixin): parent_page_types = ["main.MainHomePage"] subpage_types = ["main.MainArticlePage"] - ### PANELS - content_panels = Page.content_panels + [ - FieldPanel("perex"), - FieldPanel("shared_tags"), - ] - promote_panels = make_promote_panels() - - ### EDIT HANDLERS - - edit_handler = TabbedInterface( - [ - ObjectList(content_panels, heading="Obsah"), - ObjectList(promote_panels, heading="Propagovat"), - ObjectList(import_panels, heading="Import"), - ] - ) - - ### OTHERS - - base_form_class = JekyllImportForm - - class Meta: - verbose_name = "Rozcestník článků" - - def get_base_shared_articles_query(self, filter: models.Q): - return self.materialize_shared_articles_query( - self.append_all_shared_articles_query( - MainArticlePage.objects.filter(filter) - ) - .live() - .order_by("-union_date") - ) - - def get_article_data_list( - self, - months_back: int = 1, - search_filter: models.Q | None = None, - ): - if search_filter is None: - search_filter = models.Q() - - target_date_list = ( - self.append_all_shared_articles_query( - MainArticlePage.objects.filter(search_filter) - ) - .order_by("-union_date") - .live() - .values_list("union_date", flat=True) - ) - - if not target_date_list: - return [] - - target_date = target_date_list[0] - relativedelta(months=months_back) - first_day_of_target_month = target_date.replace(day=1) - - filter = models.Q(date__gte=first_day_of_target_month) & search_filter - - sorted_article_qs = self.get_base_shared_articles_query(filter) - - article_data_list = [] - - current_month_data = self.get_empty_month_data(timezone.now().date()) - - for article in sorted_article_qs: - if article.date.month != current_month_data["month_number"]: - if len(current_month_data["articles"]) != 0: - # append completed month if it contains any articles - article_data_list.append(current_month_data) - - current_month_data = self.get_empty_month_data(article.date) - - current_month_data["articles"].append(article) - - article_data_list.append(current_month_data) # last iteration - - return article_data_list - - def get_search_filters(self, request): - filter = models.Q() - - if "tag_id" in request.GET: - tag = self.get_filtered_tag(request) - - if tag is not None: - filter = filter & models.Q(tags__id=tag.id) - - if "q" in request.GET: - filter = filter & models.Q(title__icontains=self.get_search_query(request)) - - return filter - - def get_filtered_tag(self, request) -> Tag | None: - if "tag_id" in request.GET: - try: - return Tag.objects.filter(id=int(request.GET["tag_id"])).first() - except Exception: - pass - - return None - - def get_search_query(self, request) -> str | None: - if "q" in request.GET: - return request.GET["q"] - - def get_context(self, request, get_articles: bool = True, *args, **kwargs): - ctx = super().get_context(request, args, kwargs) - - if get_articles: - filtered_tag = self.get_filtered_tag(request) - - if filtered_tag is not None: - ctx["filtered_tag"] = filtered_tag - - search_query = self.get_search_query(request) - - if search_query is not None: - ctx["search_query"] = search_query - - search_filter = self.get_search_filters(request) - - article_timeline_list = self.get_article_data_list(1, search_filter) - - ctx["article_timeline_list"] = article_timeline_list - ctx["show_next_timeline_articles"] = len( - self.get_base_shared_articles_query(search_filter) - ) != 0 and ( - self.materialize_shared_articles_query( - self.append_all_shared_articles_query( - MainArticlePage.objects.filter(search_filter) - ) - .live() - .order_by("union_date")[:2] # LIMIT 2 - )[0] - not in article_timeline_list[-1]["articles"] - ) - - tags = [] - - for article in MainArticlePage.objects.all()[:50]: - for tag in article.tags.all(): - if tag in tags: - continue - - tags.append(tag) - - ctx["tags"] = tags - - # meow - - return ctx - - def get_timeline_articles_response(self, request): - try: - months = int(request.GET.get("months", None)) - except ValueError: - months = 1 - - search_filter = self.get_search_filters(request) - article_timeline_list = self.get_article_data_list(months, search_filter) - - context = {"article_timeline_list": article_timeline_list} - - data = { - "html": render( - request, - "main/includes/organisms/articles/articles_timeline_list.html", - context, - ).content.decode("utf-8"), - "has_next": ( - len(self.get_base_shared_articles_query(search_filter)) != 0 - and ( - self.materialize_shared_articles_query( - self.append_all_shared_articles_query( - MainArticlePage.objects.filter(search_filter) - ) - .live() - .order_by("union_date")[:2] # LIMIT 2 - )[0] - not in article_timeline_list[-1]["articles"] - ) - ), - } - - return JsonResponse(data=data, safe=False) - - @route(r"^sdilene/$", name="shared") - def shared(self, request): - return self.setup_article_page_context(request) - - def serve(self, request, *args, **kwargs): - if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest": - if "months" in request.GET: - return self.get_timeline_articles_response(request) - - return super().serve(request, *args, **kwargs) - - @staticmethod - def get_empty_month_data(date_obj): - return { - "month_number": date_obj.month, - "month_text": MONTH_NAMES[date_obj.month - 1], - "articles": [], - } - class MainArticleTag(TaggedItemBase): content_object = ParentalKey( @@ -382,29 +140,7 @@ class MainArticleTag(TaggedItemBase): ) -class MainArticlePage( - ArticleMixin, - ExtendedMetadataPageMixin, - SubpageMixin, - MetadataPageMixin, - PageInMenuMixin, - Page, -): - ### FIELDS - content = StreamField( - [ - ( - "text", - RichTextBlock(template="main/includes/atoms/text/prose_richtext.html"), - ), - ("quote", blocks.ArticleQuoteBlock()), - ("download", blocks.ArticleDownloadBlock()), - ], - verbose_name="Článek", - blank=True, - use_json_field=True, - ) - +class MainArticlePage(MainArticlePageMixin): author_page = models.ForeignKey( "main.MainPersonPage", on_delete=models.SET_NULL, @@ -415,50 +151,10 @@ class MainArticlePage( tags = ClusterTaggableManager( through=MainArticleTag, related_name="tagged_articles", blank=True ) - shared_tags = ClusterTaggableManager( - verbose_name="Tagy pro sdílení mezi weby", - through=SharedTaggedMainArticle, - blank=True, - ) - - search_fields = ArticleMixin.search_fields + [ - index.SearchField("author_page"), - index.FilterField("slug"), - ] - - ### PANELS - - content_panels = ArticleMixin.content_panels + [ - FieldPanel("author_page"), - FieldPanel("tags"), - FieldPanel("shared_tags"), - ] - - promote_panels = make_promote_panels( - admin_help.build(admin_help.NO_SEO_TITLE, admin_help.NO_DESCRIPTION_USE_PEREX), - search_image=False, - ) - - ### RELATIONS parent_page_types = ["main.MainArticlesPage"] subpage_types = [] - ### OTHERS - - class Meta: - verbose_name = "Aktualita" - - # def get_context(self, request): chceme/nechceme? - # context = super().get_context(request) - # context["related_articles"] = ( - # self.get_siblings(inclusive=False) - # .live() - # .specific() - # .order_by("-mainarticlepage__date")[:3] - # ) - # return context - class MainProgramPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page diff --git a/shared/blocks/main.py b/shared/blocks/main.py index 9cce950f..43eefe90 100644 --- a/shared/blocks/main.py +++ b/shared/blocks/main.py @@ -1,5 +1,4 @@ from wagtail import blocks - from wagtail.blocks import ( CharBlock, ListBlock, @@ -8,12 +7,13 @@ from wagtail.blocks import ( StructBlock, URLBlock, ) +from wagtail.documents.blocks import DocumentChooserBlock from .base import MenuItemBlock as MenuItemBlockBase - # Mixins (or used as such) + class CTAMixin(StructBlock): button_link = URLBlock(label="Odkaz tlačítka") button_text = CharBlock(label="Text tlačítka") @@ -34,6 +34,7 @@ class LinkBlock(StructBlock): # Navbar + class MainMenuItemBlock(MenuItemBlockBase): title = blocks.CharBlock( label="Titulek", @@ -63,8 +64,31 @@ class SocialLinkBlock(LinkBlock): label = "Odkaz" +# Articles + + +class ArticleQuoteBlock(StructBlock): + quote = CharBlock(label="Citace") + autor_name = CharBlock(label="Jméno autora") + + class Meta: + icon = "user" + label = "Blok citace" + template = "main/includes/legacy/article_quote_block.html" + + +class ArticleDownloadBlock(StructBlock): + file = DocumentChooserBlock(label="Stáhnutelný soubor") + + class Meta: + icon = "user" + label = "Blok stáhnutelného dokumentu" + template = "main/includes/molecules/blocks/article_download_block.html" + + # People + class PersonContactBlock(StructBlock): position = CharBlock(label="Název pozice", required=False) # email, phone? @@ -80,6 +104,7 @@ class PersonContactBlock(StructBlock): # Footer + class OtherLinksBlock(StructBlock): title = CharBlock(label="Titulek") list = ListBlock(LinkBlock, label="Seznam odkazů s titulkem") diff --git a/shared/const.py b/shared/const.py index d2f6a034..af05b79c 100644 --- a/shared/const.py +++ b/shared/const.py @@ -17,3 +17,18 @@ RICH_TEXT_DEFAULT_FEATURES = [ "blockquote", "embed", ] + +MONTH_NAMES = [ + "Leden", + "Únor", + "Březen", + "Duben", + "Květen", + "Červen", + "Červenec", + "Srpen", + "Září", + "Říjen", + "Listopad", + "Prosinec", +] diff --git a/shared/forms.py b/shared/forms.py index 98a3256a..f93c09f7 100644 --- a/shared/forms.py +++ b/shared/forms.py @@ -1,7 +1,86 @@ from django import forms +from wagtail.admin.forms import WagtailAdminPageForm +from wagtail.models.collections import Collection + +from shared.jekyll_import import JekyllArticleImporter class SubscribeForm(forms.Form): email = forms.EmailField() confirmed = forms.BooleanField() return_page_id = forms.IntegerField() + + +class JekyllImportForm(WagtailAdminPageForm): + do_import = forms.BooleanField( + initial=False, required=False, label="Provést import z Jekyllu" + ) + collection = forms.ModelChoiceField( + queryset=Collection.objects.all(), required=False, label="Kolekce obrázků" + ) + dry_run = forms.BooleanField( + initial=True, + required=False, + label="Jenom na zkoušku", + help_text="Žádné články se neuloží, vypíše případné problémy či " + "již existující články - 'ostrému' importu existující " + "články nevadí, přeskočí je", + ) + jekyll_repo_url = forms.URLField( + max_length=512, + required=False, + help_text="např. https://github.com/pirati-web/pirati.cz", + ) + readonly_log = forms.CharField( + disabled=True, + label="Log z posledního importu", + required=False, + widget=forms.Textarea, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["readonly_log"].initial = self.instance.last_import_log + + def clean(self): + cleaned_data = super().clean() + + if not cleaned_data.get("do_import"): + return cleaned_data + + if cleaned_data.get("do_import") and not self.instance.id: + self.add_error( + "do_import", "Import proveďte prosím až po vytvoření stránky" + ) + + if not cleaned_data.get("collection"): + self.add_error("collection", "Pro import je toto pole povinné") + if not cleaned_data.get("jekyll_repo_url"): + self.add_error("jekyll_repo_url", "Pro import je toto pole povinné") + + if cleaned_data.get("jekyll_repo_url", "").endswith(".zip"): + self.add_error( + "jekyll_repo_url", "Vložte odkaz pouze na repozitář, ne na zip" + ) + + return cleaned_data + + def handle_import(self, model): + # TODO: Portable function + + from .models import MainArticlePage + + JekyllArticleImporter( + article_parent_page=self.instance, + collection_id=self.cleaned_data["collection"].id, + url=self.cleaned_data["jekyll_repo_url"], + dry_run=self.cleaned_data["dry_run"], + use_git=True, + page_model=MainArticlePage, + ).perform_import() + + def save(self, commit=True): + if self.cleaned_data.get("do_import"): + self.handle_import() + + return super().save(commit=commit) diff --git a/shared/models/main.py b/shared/models/main.py index fd3d0c52..29823a91 100644 --- a/shared/models/main.py +++ b/shared/models/main.py @@ -29,28 +29,29 @@ from wagtail.search import index from wagtailmetadata.models import MetadataPageMixin from calendar_utils.models import CalendarMixin -from shared.forms import SubscribeForm +from shared.blocks import ( + ArticleDownloadBlock, + ArticleQuoteBlock, + MainMenuItemBlock, + NavbarMenuItemBlock, + OtherLinksBlock, + PersonContactBlock, + SocialLinkBlock, +) +from shared.const import MONTH_NAMES +from shared.forms import JekyllImportForm, SubscribeForm +from shared.utils import make_promote_panels, subscribe_to_newsletter +from tuning import admin_help + from .base import ( # MenuMixin, ArticleMixin, ArticlesMixin, ArticlesPageMixin, ExtendedMetadataHomePageMixin, ExtendedMetadataPageMixin, - SharedTaggedMainArticle, - SubpageMixin, ) -from shared.utils import make_promote_panels, subscribe_to_newsletter -from tuning import admin_help - -from wagtail.models import Page - -from django.db import models -from wagtail.admin.panels import FieldPanel, MultiFieldPanel -from wagtail.fields import StreamField -from wagtail.models import Page - from .base import MenuMixin as MenuMixinBase -from shared.blocks import MainMenuItemBlock, NavbarMenuItemBlock, OtherLinksBlock, PersonContactBlock, SocialLinkBlock +from .base import SharedTaggedMainArticle, SubpageMixin # MenuMixin, class MainMenuMixin(MenuMixinBase): @@ -261,7 +262,9 @@ class MainHomePageMixin( context = super().get_context(request, args, kwargs) context["article_data_list"] = self.materialize_shared_articles_query( - self.append_all_shared_articles_query(self.article_page_model.objects.live().all()) + self.append_all_shared_articles_query( + self.article_page_model.objects.live().all() + ) .live() .order_by("-union_date")[:3] ) @@ -373,3 +376,316 @@ class MainHomePageMixin( @route(r"^sdilene/$", name="shared") def shared(self, request): return self.setup_article_page_context(request) + + +class MainArticlesPageMixin( + RoutablePageMixin, + ExtendedMetadataPageMixin, + SubpageMixin, + MetadataPageMixin, + ArticlesPageMixin, + PageInMenuMixin, + Page, +): + last_import_log = models.TextField( + "Výstup z posledního importu", null=True, blank=True + ) + perex = models.TextField() + + import_panels = [ + MultiFieldPanel( + [ + FieldPanel("do_import"), + FieldPanel("collection"), + FieldPanel("dry_run"), + FieldPanel("jekyll_repo_url"), + FieldPanel("readonly_log"), + HelpPanel( + "Import provádějte vždy až po vytvoření stránky aktualit. " + 'Pro uložení logu je nutné volit možnost "Publikovat", nikoliv' + 'pouze "Uložit koncept". ' + "Import proběhne na pozadí a může trvat až několik minut. " + "Dejte si po spuštění importu kávu a potom obnovte stránku pro " + "zobrazení výsledku importu." + ), + ], + "import z Jekyll repozitáře", + ), + ] + + ### RELATIONS + + parent_page_types = [] # NOTE: Must be implemented + subpage_types = [] # NOTE: Must be implemented + + ### PANELS + content_panels = Page.content_panels + [ + FieldPanel("perex"), + FieldPanel("shared_tags"), + ] + promote_panels = make_promote_panels() + + ### EDIT HANDLERS + + edit_handler = TabbedInterface( + [ + ObjectList(content_panels, heading="Obsah"), + ObjectList(promote_panels, heading="Propagovat"), + ObjectList(import_panels, heading="Import"), + ] + ) + + ### OTHERS + + base_form_class = JekyllImportForm + + class Meta: + verbose_name = "Rozcestník článků" + abstract = True + + def get_base_shared_articles_query(self, filter: models.Q): + return self.materialize_shared_articles_query( + self.append_all_shared_articles_query( + self.root_page.article_page_model.objects.filter(filter) + ) + .live() + .order_by("-union_date") + ) + + def get_article_data_list( + self, + months_back: int = 1, + search_filter: models.Q | None = None, + ): + if search_filter is None: + search_filter = models.Q() + + target_date_list = ( + self.append_all_shared_articles_query( + self.root_page.article_page_model.objects.filter(search_filter) + ) + .order_by("-union_date") + .live() + .values_list("union_date", flat=True) + ) + + if not target_date_list: + return [] + + target_date = target_date_list[0] - relativedelta(months=months_back) + first_day_of_target_month = target_date.replace(day=1) + + filter = models.Q(date__gte=first_day_of_target_month) & search_filter + + sorted_article_qs = self.get_base_shared_articles_query(filter) + + article_data_list = [] + + current_month_data = self.get_empty_month_data(timezone.now().date()) + + for article in sorted_article_qs: + if article.date.month != current_month_data["month_number"]: + if len(current_month_data["articles"]) != 0: + # append completed month if it contains any articles + article_data_list.append(current_month_data) + + current_month_data = self.get_empty_month_data(article.date) + + current_month_data["articles"].append(article) + + article_data_list.append(current_month_data) # last iteration + + return article_data_list + + def get_search_filters(self, request): + filter = models.Q() + + if "tag_id" in request.GET: + tag = self.get_filtered_tag(request) + + if tag is not None: + filter = filter & models.Q(tags__id=tag.id) + + if "q" in request.GET: + filter = filter & models.Q(title__icontains=self.get_search_query(request)) + + return filter + + def get_filtered_tag(self, request) -> Tag | None: + if "tag_id" in request.GET: + try: + return Tag.objects.filter(id=int(request.GET["tag_id"])).first() + except Exception: + pass + + return None + + def get_search_query(self, request) -> str | None: + if "q" in request.GET: + return request.GET["q"] + + def get_context(self, request, get_articles: bool = True, *args, **kwargs): + ctx = super().get_context(request, args, kwargs) + + if get_articles: + filtered_tag = self.get_filtered_tag(request) + + if filtered_tag is not None: + ctx["filtered_tag"] = filtered_tag + + search_query = self.get_search_query(request) + + if search_query is not None: + ctx["search_query"] = search_query + + search_filter = self.get_search_filters(request) + + article_timeline_list = self.get_article_data_list(1, search_filter) + + ctx["article_timeline_list"] = article_timeline_list + ctx["show_next_timeline_articles"] = len( + self.get_base_shared_articles_query(search_filter) + ) != 0 and ( + self.materialize_shared_articles_query( + self.append_all_shared_articles_query( + self.root_page.article_page_model.objects.filter(search_filter) + ) + .live() + .order_by("union_date")[:2] # LIMIT 2 + )[0] + not in article_timeline_list[-1]["articles"] + ) + + tags = [] + + for article in self.root_page.article_page_model.objects.all()[:50]: + for tag in article.tags.all(): + if tag in tags: + continue + + tags.append(tag) + + ctx["tags"] = tags + + # meow + + return ctx + + def get_timeline_articles_response(self, request): + try: + months = int(request.GET.get("months", None)) + except ValueError: + months = 1 + + search_filter = self.get_search_filters(request) + article_timeline_list = self.get_article_data_list(months, search_filter) + + context = {"article_timeline_list": article_timeline_list} + + data = { + "html": render( + request, + "main/includes/organisms/articles/articles_timeline_list.html", + context, + ).content.decode("utf-8"), + "has_next": ( + len(self.get_base_shared_articles_query(search_filter)) != 0 + and ( + self.materialize_shared_articles_query( + self.append_all_shared_articles_query( + self.root_page.article_page_model.objects.filter( + search_filter + ) + ) + .live() + .order_by("union_date")[:2] # LIMIT 2 + )[0] + not in article_timeline_list[-1]["articles"] + ) + ), + } + + return JsonResponse(data=data, safe=False) + + @route(r"^sdilene/$", name="shared") + def shared(self, request): + return self.setup_article_page_context(request) + + def serve(self, request, *args, **kwargs): + if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest": + if "months" in request.GET: + return self.get_timeline_articles_response(request) + + return super().serve(request, *args, **kwargs) + + @staticmethod + def get_empty_month_data(date_obj): + return { + "month_number": date_obj.month, + "month_text": MONTH_NAMES[date_obj.month - 1], + "articles": [], + } + + +class MainArticlePageMixin( + ArticleMixin, + ExtendedMetadataPageMixin, + SubpageMixin, + MetadataPageMixin, + PageInMenuMixin, + Page, +): + ### FIELDS + content = StreamField( + [ + ( + "text", + RichTextBlock(template="main/includes/atoms/text/prose_richtext.html"), + ), + ("quote", ArticleQuoteBlock()), + ("download", ArticleDownloadBlock()), + ], + verbose_name="Článek", + blank=True, + use_json_field=True, + ) + + @property + def tags(self): + # NOTE: Must be implemented + raise NotImplementedError + + shared_tags = ClusterTaggableManager( + verbose_name="Tagy pro sdílení mezi weby", + through=SharedTaggedMainArticle, + blank=True, + ) + + search_fields = ArticleMixin.search_fields + [ + index.SearchField("author_page"), + index.FilterField("slug"), + ] + + ### PANELS + + content_panels = ArticleMixin.content_panels + [ + FieldPanel("author_page"), + FieldPanel("tags"), + FieldPanel("shared_tags"), + ] + + promote_panels = make_promote_panels( + admin_help.build(admin_help.NO_SEO_TITLE, admin_help.NO_DESCRIPTION_USE_PEREX), + search_image=False, + ) + + ### RELATIONS + + parent_page_types = [] # NOTE: Must be implemented + subpage_types = [] # NOTE: Must be implemented + + ### OTHERS + + class Meta: + verbose_name = "Aktualita" + abstract = True -- GitLab