From f331981c2bdaefae400fb9631fa18d1bc522a4d7 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:09:17 +0100
Subject: [PATCH] share main models

---
 main/blocks.py                                |  68 +---
 ...5_alter_mainhomepage_footer_other_links.py |  41 ++
 main/models.py                                | 203 +---------
 shared/blocks/__init__.py                     |   2 +
 shared/{blocks.py => blocks/base.py}          |   0
 shared/blocks/main.py                         |  90 +++++
 {main => shared}/feeds.py                     |  36 +-
 shared/models/__init__.py                     |   2 +
 shared/{models.py => models/base.py}          |   0
 shared/models/main.py                         | 375 ++++++++++++++++++
 10 files changed, 553 insertions(+), 264 deletions(-)
 create mode 100644 main/migrations/0065_alter_mainhomepage_footer_other_links.py
 create mode 100644 shared/blocks/__init__.py
 rename shared/{blocks.py => blocks/base.py} (100%)
 create mode 100644 shared/blocks/main.py
 rename {main => shared}/feeds.py (57%)
 create mode 100644 shared/models/__init__.py
 rename shared/{models.py => models/base.py} (100%)
 create mode 100644 shared/models/main.py

diff --git a/main/blocks.py b/main/blocks.py
index a489dfe8..faac594d 100644
--- a/main/blocks.py
+++ b/main/blocks.py
@@ -12,7 +12,7 @@ from wagtail.blocks import (
 from wagtail.documents.blocks import DocumentChooserBlock
 from wagtail.images.blocks import ImageChooserBlock
 
-from shared.blocks import CardLinkBlockMixin, CardLinkWithHeadlineBlockMixin
+from shared.blocks import CardLinkBlockMixin, CardLinkWithHeadlineBlockMixin, CTAMixin
 
 PROGRAM_RICH_TEXT_FEATURES = [
     "h3",
@@ -56,21 +56,6 @@ class CardLinkBlock(CardLinkBlockMixin):
         label = "Karta s odkazem"
 
 
-class CTAMixin(StructBlock):
-    button_link = URLBlock(label="Odkaz tlačítka")
-    button_text = CharBlock(label="Text tlačítka")
-
-    class Meta:
-        icon = "doc-empty"
-        label = "Výzva s odkazem"
-
-
-class NavbarMenuItemBlock(CTAMixin):
-    class Meta:
-        label = "Tlačítko"
-        template = "main/includes/molecules/navbar/additional_button.html"
-
-
 class ProgramGroupBlockMixin(StructBlock):
     title = CharBlock(label="Titulek části programu")
     # point_list = ListBlock(ProgramBlock(), label="Jednotlivé články programu")
@@ -254,19 +239,6 @@ class RegionsBlock(StructBlock):
         label = "Články pro regiony"
 
 
-class PersonContactBlock(StructBlock):
-    position = CharBlock(label="Název pozice", required=False)
-    # email, phone?
-    person = PageChooserBlock(
-        label="Osoba",
-        page_type=["main.MainPersonPage"],
-    )
-
-    class Meta:
-        icon = "user"
-        label = "Osoba s volitelnou pozicí"
-
-
 class PersonContactBoxBlock(StructBlock):
     title = CharBlock(label="Titulek")
     image = ImageChooserBlock(label="Ikona")
@@ -277,38 +249,6 @@ class PersonContactBoxBlock(StructBlock):
         label = "Kontakty"
 
 
-# Footer
-class LinkBlock(StructBlock):
-    text = CharBlock(label="Název")
-    link = URLBlock(label="Odkaz")
-
-    class Meta:
-        icon = "link"
-        label = "Odkaz"
-
-
-class OtherLinksBlock(StructBlock):
-    title = CharBlock(label="Titulek")
-    list = ListBlock(LinkBlock, label="Seznam odkazů s titulkem")
-
-    class Meta:
-        icon = "link"
-        label = "Odkazy"
-        template = "main/blocks/article_quote_block.html"
-
-
-class SocialLinkBlock(LinkBlock):
-    icon = CharBlock(
-        label="Ikona",
-        help_text="Seznam ikon - https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons <br/>"
-        "Název ikony zadejte bez tečky na začátku",
-    )  # TODO CSS class name or somthing better?
-
-    class Meta:
-        icon = "link"
-        label = "Odkaz"
-
-
 # ARTICLE BLOCKS
 class ArticleQuoteBlock(StructBlock):
     quote = CharBlock(label="Citace")
@@ -408,3 +348,9 @@ class TeamBlock(StructBlock):
         value = super().get_prep_value(value)
         value["slug"] = slugify(value["title"])
         return value
+
+
+# --- 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
new file mode 100644
index 00000000..787c9193
--- /dev/null
+++ b/main/migrations/0065_alter_mainhomepage_footer_other_links.py
@@ -0,0 +1,41 @@
+# 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
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("main", "0064_alter_mainhomepage_content"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="mainhomepage",
+            name="footer_other_links",
+            field=wagtail.fields.StreamField(
+                [
+                    (
+                        "other_links",
+                        wagtail.blocks.StructBlock(
+                            [
+                                ("title", wagtail.blocks.CharBlock(label="Titulek")),
+                                (
+                                    "list",
+                                    wagtail.blocks.ListBlock(
+                                        shared.blocks.main.LinkBlock,
+                                        label="Seznam odkazů s titulkem",
+                                    ),
+                                ),
+                            ]
+                        ),
+                    )
+                ],
+                blank=True,
+                use_json_field=True,
+                verbose_name="Odkazy v zápatí webu",
+            ),
+        ),
+    ]
diff --git a/main/models.py b/main/models.py
index 34b23630..65cb63fa 100644
--- a/main/models.py
+++ b/main/models.py
@@ -40,6 +40,7 @@ from shared.models import (  # MenuMixin,
     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
@@ -48,29 +49,10 @@ from .forms import JekyllImportForm
 from .menu import MenuMixin, PageInMenuMixin
 
 
-class MainHomePage(
-    MenuMixin,
-    RoutablePageMixin,
-    ExtendedMetadataHomePageMixin,
-    MetadataPageMixin,
-    ArticlesMixin,
-    Page,
-):
-    # header
+from shared.models import MainHomePageMixin
 
-    menu_button_name = models.CharField(
-        verbose_name="Text na tlačítku pro zapojení", max_length=16
-    )
-
-    menu_button_content = StreamField(
-        [
-            ("navbar_menu_item", blocks.NavbarMenuItemBlock()),
-        ],
-        verbose_name="Obsah menu pro zapojení se",
-        blank=True,
-        use_json_field=True,
-    )
 
+class MainHomePage(MainHomePageMixin):
     # content
     content = StreamField(
         [
@@ -85,25 +67,6 @@ class MainHomePage(
         blank=True,
         use_json_field=True,
     )
-
-    # footer
-    footer_other_links = StreamField(
-        [
-            ("other_links", blocks.OtherLinksBlock()),
-        ],
-        verbose_name="Odkazy v zápatí webu",
-        blank=True,
-        use_json_field=True,
-    )
-
-    footer_person_list = StreamField(
-        [("person", blocks.PersonContactBlock())],
-        verbose_name="Osoby v zápatí webu",
-        blank=True,
-        max_num=6,
-        use_json_field=True,
-    )
-
     # settings
     gdpr_and_cookies_page = models.ForeignKey(
         "main.MainSimplePage",
@@ -113,48 +76,6 @@ class MainHomePage(
         null=True,
     )
 
-    matomo_id = models.IntegerField(
-        "Matomo ID pro sledování návštěvnosti", blank=True, null=True
-    )
-
-    social_links = StreamField(
-        [
-            ("social_links", blocks.SocialLinkBlock()),
-        ],
-        verbose_name="Odkazy na sociální sítě",
-        blank=True,
-        use_json_field=True,
-    )
-
-    content_panels = Page.content_panels + [
-        FieldPanel("content"),
-        FieldPanel("footer_other_links"),
-        FieldPanel("footer_person_list"),
-    ]
-
-    promote_panels = make_promote_panels(admin_help.build(admin_help.IMPORTANT_TITLE))
-
-    settings_panels = [
-        FieldPanel("menu_button_name"),
-        FieldPanel("menu_button_content"),
-        PageChooserPanel("gdpr_and_cookies_page"),
-        FieldPanel("social_links"),
-        FieldPanel("matomo_id"),
-    ]
-
-    ### EDIT HANDLERS
-
-    edit_handler = TabbedInterface(
-        [
-            ObjectList(content_panels, heading="Obsah"),
-            ObjectList(promote_panels, heading="Propagovat"),
-            ObjectList(settings_panels, heading="Nastavení"),
-            ObjectList(MenuMixin.menu_panels, heading="Menu"),
-        ]
-    )
-
-    ### RELATIONS
-
     subpage_types = [
         "main.MainArticlesPage",
         "main.MainProgramPage",
@@ -172,122 +93,30 @@ class MainHomePage(
     class Meta:
         verbose_name = "HomePage pirati.cz"
 
-    @cached_property
-    def gdpr_and_cookies_url(self):
-        if self.gdpr_and_cookies_page:
-            return self.gdpr_and_cookies_page.url
-        return "#"
-
-    @staticmethod
-    def get_404_response(request):
-        return render(request, "main/404.html", status=404)
-
-    def get_context(self, request, *args, **kwargs):
-        context = super().get_context(request, args, kwargs)
-
-        context["article_data_list"] = self.materialize_shared_articles_query(
-            self.append_all_shared_articles_query(MainArticlePage.objects.live().all())
-            .live()
-            .order_by("-union_date")[:3]
-        )
-
-        return context
-
-    def get_region_response(self, request):
-        context = {}
-        if request.GET.get("region", None) == "VSK":
-            sorted_article_qs = MainArticlePage.objects.filter(
-                region__isnull=False
-            ).order_by("-date")
-            context = {"article_data_list": sorted_article_qs[:3]}
-        else:
-            sorted_article_qs = MainArticlePage.objects.filter(
-                region=request.GET.get("region", None)
-            )[:3]
-            context = {"article_data_list": sorted_article_qs[:3]}
-
-        data = {
-            "html": render(
-                request, "main/includes/small_article_preview.html", context
-            ).content.decode("utf-8")
-        }
-
-        return JsonResponse(data=data, safe=False)
-
-    def serve(self, request, *args, **kwargs):
-        if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
-            if "region" in request.GET:
-                return self.get_region_response(request)
-
-        return super().serve(request, *args, **kwargs)
-
-    @cached_property
-    def newsletter_subscribe_url(self):
-        newsletter_subscribe = self.reverse_subpage("newsletter_subscribe")
-        return (
-            self.url + newsletter_subscribe
-            if self.url is not None
-            else newsletter_subscribe
-        )  # preview fix
+    @property
+    def article_page_model(self):
+        return MainArticlePage
 
     @property
-    def articles_page(self):
-        return self._first_subpage_of_type(MainArticlesPage)
+    def articles_page_model(self):
+        return MainArticlesPage
 
     @property
-    def people_page(self):
-        return self._first_subpage_of_type(MainPeoplePage)
+    def contact_page_model(self):
+        return MainContactPage
 
     @property
-    def contact_page(self):
-        return self._first_subpage_of_type(MainContactPage)
+    def search_page_model(self):
+        return MainSearchPage
 
     @property
-    def search_page(self):
-        return self._first_subpage_of_type(MainSearchPage)
+    def people_page(self):
+        return self._first_subpage_of_type(MainPeoplePage)
 
     @property
     def root_page(self):
         return self
 
-    @route(r"^prihlaseni-k-newsletteru/$")
-    def newsletter_subscribe(self, request):
-        if request.method == "POST":
-            form = SubscribeForm(request.POST)
-            if form.is_valid():
-                subscribe_to_newsletter(
-                    form.cleaned_data["email"], settings.PIRATICZ_NEWSLETTER_CID
-                )
-
-                messages.success(
-                    request,
-                    "Zkontroluj si prosím schránku, poslali jsme ti potvrzovací email.",
-                )
-
-                try:
-                    page = (
-                        Page.objects.filter(id=form.cleaned_data["return_page_id"])
-                        .live()
-                        .first()
-                    )
-                    return HttpResponseRedirect(page.full_url)
-                except Page.DoesNotExist:
-                    return HttpResponseRedirect(self.url)
-
-            messages.error(
-                request,
-                "Tvůj prohlížeč nám odeslal špatná data. Prosím, zkus to znovu.",
-            )
-
-        return HttpResponseRedirect(self.url)
-
-    @route(r"^feeds/atom/$")
-    def view_feed(self, request):
-        # Avoid circular import
-        from .feeds import LatestArticlesFeed  # noqa
-
-        return LatestArticlesFeed()(request, self.articles_page.id)
-
     def _first_subpage_of_type(self, page_type) -> Page or None:
         try:
             return self.get_descendants().type(page_type).live().specific()[0]
@@ -765,7 +594,7 @@ class MainPersonPage(
 
     social_links = StreamField(
         [
-            ("social_links", blocks.SocialLinkBlock()),
+            ("social_links", shared_blocks.SocialLinkBlock()),
         ],
         verbose_name="Odkazy na sociální sítě",
         blank=True,
@@ -886,7 +715,7 @@ class MainContactPage(
     ### FIELDS
 
     contact_people = StreamField(
-        [("item", blocks.PersonContactBlock())],
+        [("item", shared_blocks.PersonContactBlock())],
         verbose_name="Kontaktní osoby",
         blank=True,
         use_json_field=True,
diff --git a/shared/blocks/__init__.py b/shared/blocks/__init__.py
new file mode 100644
index 00000000..d31603b8
--- /dev/null
+++ b/shared/blocks/__init__.py
@@ -0,0 +1,2 @@
+from .base import *
+from .main import *
diff --git a/shared/blocks.py b/shared/blocks/base.py
similarity index 100%
rename from shared/blocks.py
rename to shared/blocks/base.py
diff --git a/shared/blocks/main.py b/shared/blocks/main.py
new file mode 100644
index 00000000..9cce950f
--- /dev/null
+++ b/shared/blocks/main.py
@@ -0,0 +1,90 @@
+from wagtail import blocks
+
+from wagtail.blocks import (
+    CharBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    URLBlock,
+)
+
+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")
+
+    class Meta:
+        icon = "doc-empty"
+        label = "Výzva s odkazem"
+
+
+class LinkBlock(StructBlock):
+    text = CharBlock(label="Název")
+    link = URLBlock(label="Odkaz")
+
+    class Meta:
+        icon = "link"
+        label = "Odkaz"
+
+
+# Navbar
+
+class MainMenuItemBlock(MenuItemBlockBase):
+    title = blocks.CharBlock(
+        label="Titulek",
+        help_text="Pokud není odkazovaná stránka na Majáku, použij možnost zadání samotné adresy níže.",
+        required=True,
+    )
+
+    class Meta:
+        label = "Položka v menu"
+
+
+class NavbarMenuItemBlock(CTAMixin):
+    class Meta:
+        label = "Tlačítko"
+        template = "main/includes/molecules/navbar/additional_button.html"
+
+
+class SocialLinkBlock(LinkBlock):
+    icon = CharBlock(
+        label="Ikona",
+        help_text="Seznam ikon - https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons <br/>"
+        "Název ikony zadejte bez tečky na začátku",
+    )  # TODO CSS class name or somthing better?
+
+    class Meta:
+        icon = "link"
+        label = "Odkaz"
+
+
+# People
+
+class PersonContactBlock(StructBlock):
+    position = CharBlock(label="Název pozice", required=False)
+    # email, phone?
+    person = PageChooserBlock(
+        label="Osoba",
+        page_type=["main.MainPersonPage"],
+    )
+
+    class Meta:
+        icon = "user"
+        label = "Osoba s volitelnou pozicí"
+
+
+# Footer
+
+class OtherLinksBlock(StructBlock):
+    title = CharBlock(label="Titulek")
+    list = ListBlock(LinkBlock, label="Seznam odkazů s titulkem")
+
+    class Meta:
+        icon = "link"
+        label = "Odkazy"
+        template = "main/blocks/article_quote_block.html"
diff --git a/main/feeds.py b/shared/feeds.py
similarity index 57%
rename from main/feeds.py
rename to shared/feeds.py
index 54438673..7165433b 100644
--- a/main/feeds.py
+++ b/shared/feeds.py
@@ -5,39 +5,43 @@ from django.contrib.syndication.views import Feed
 from django.template.loader import render_to_string
 from django.urls import reverse
 
-from .models import MainArticlePage, MainArticlesPage
-
 
 class LatestArticlesFeed(Feed):
-    def get_object(self, request, id: int) -> MainArticlesPage:
-        return MainArticlesPage.objects.get(id=id)
+    def __init__(self, articles_page_model, article_page_model, *args, **kwargs):
+        self.articles_page_model = articles_page_model
+        self.article_page_model = article_page_model
+
+        return super().__init__(*args, **kwargs)
+
+    def get_object(self, request, id: int):
+        return self.articles_page_model.objects.get(id=id)
 
-    def title(self, obj: MainArticlesPage) -> str:
+    def title(self, obj) -> str:
         return obj.title
 
-    def link(self, obj: MainArticlesPage) -> str:
+    def link(self, obj) -> str:
         return obj.get_full_url()
 
-    def description(self, obj: MainArticlesPage) -> str:
+    def description(self, obj) -> str:
         return obj.perex
 
-    def items(self, obj: MainArticlesPage) -> list:
+    def items(self, obj) -> list:
         return obj.materialize_shared_articles_query(
             obj.append_all_shared_articles_query(MainArticlePage.objects.child_of(obj))[
                 :32
             ]
         )
 
-    def item_title(self, item: MainArticlePage) -> str:
+    def item_title(self, item) -> str:
         return item.title
 
-    def item_description(self, item: MainArticlePage) -> str:
+    def item_description(self, item) -> str:
         return render_to_string(
             "main/feed_item_description.html",
             {"item": item},
         )
 
-    def item_pubdate(self, item: MainArticlePage) -> datetime:
+    def item_pubdate(self, item) -> datetime:
         return datetime(
             item.date.year,
             item.date.month,
@@ -46,7 +50,7 @@ class LatestArticlesFeed(Feed):
             0,
         )
 
-    def item_author_name(self, item: MainArticlePage) -> str:
+    def item_author_name(self, item) -> str:
         if item.author:
             return item.author
 
@@ -55,13 +59,13 @@ class LatestArticlesFeed(Feed):
 
         return ""
 
-    def item_categories(self, item: MainArticlePage) -> list:
+    def item_categories(self, item) -> list:
         return item.get_tags
 
-    def item_link(self, item: MainArticlePage) -> str:
+    def item_link(self, item) -> str:
         return item.get_full_url()
 
-    def item_enclosure_url(self, item: MainArticlePage) -> typing.Union[None, str]:
+    def item_enclosure_url(self, item) -> typing.Union[None, str]:
         if item.image is None:
             return None
 
@@ -69,5 +73,5 @@ class LatestArticlesFeed(Feed):
 
     item_enclosure_mime_type = "image/webp"
 
-    def item_enclosure_length(self, item: MainArticlePage) -> int:
+    def item_enclosure_length(self, item) -> int:
         return item.image.file_size
diff --git a/shared/models/__init__.py b/shared/models/__init__.py
new file mode 100644
index 00000000..d31603b8
--- /dev/null
+++ b/shared/models/__init__.py
@@ -0,0 +1,2 @@
+from .base import *
+from .main import *
diff --git a/shared/models.py b/shared/models/base.py
similarity index 100%
rename from shared/models.py
rename to shared/models/base.py
diff --git a/shared/models/main.py b/shared/models/main.py
new file mode 100644
index 00000000..fd3d0c52
--- /dev/null
+++ b/shared/models/main.py
@@ -0,0 +1,375 @@
+import datetime
+from functools import cached_property
+
+from dateutil.relativedelta import relativedelta
+from django.conf import settings
+from django.contrib import messages
+from django.core.paginator import Paginator
+from django.db import models
+from django.forms.models import model_to_dict
+from django.http import HttpResponseRedirect, JsonResponse
+from django.shortcuts import render
+from django.utils import timezone
+from modelcluster.contrib.taggit import ClusterTaggableManager
+from modelcluster.fields import ParentalKey
+from taggit.models import Tag, TaggedItemBase
+from wagtail.admin.panels import (
+    FieldPanel,
+    HelpPanel,
+    MultiFieldPanel,
+    ObjectList,
+    PageChooserPanel,
+    TabbedInterface,
+)
+from wagtail.blocks import PageChooserBlock, RichTextBlock
+from wagtail.contrib.routable_page.models import RoutablePageMixin, route
+from wagtail.fields import RichTextField, StreamField
+from wagtail.models import Page
+from wagtail.search import index
+from wagtailmetadata.models import MetadataPageMixin
+
+from calendar_utils.models import CalendarMixin
+from shared.forms import SubscribeForm
+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
+
+
+class MainMenuMixin(MenuMixinBase):
+    important_item_name = models.CharField(
+        verbose_name="Jméno",
+        help_text="Pokud není odkazovaná stránka na Majáku, použij možnost zadání samotné adresy níže.",
+        max_length=16,
+        blank=True,
+        null=True,
+    )
+
+    important_item_page = models.ForeignKey(
+        Page,
+        verbose_name="Stránka",
+        null=True,
+        blank=True,
+        related_name="+",
+        on_delete=models.PROTECT,
+    )
+
+    important_item_url = models.URLField(
+        verbose_name="Adresa",
+        blank=True,
+        null=True,
+    )
+
+    menu = StreamField(
+        [("menu_item", MainMenuItemBlock())],  # , ("menu_parent", MenuParentBlock())
+        verbose_name="Položky",
+        blank=True,
+        use_json_field=True,
+    )
+
+    menu_panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel("important_item_name"),
+                FieldPanel("important_item_page"),
+                FieldPanel("important_item_url"),
+            ],
+            heading="Důležitá položka menu",
+        ),
+        MultiFieldPanel(
+            [
+                FieldPanel("menu"),
+            ],
+            heading="Další obsah menu",
+        ),
+    ]
+
+    class Meta:
+        abstract = True
+
+
+class PageInMenuMixin(Page):
+    def get_menu_title(self) -> str:
+        for menu_item in self.root_page.menu:
+            if menu_item.value["page"] is None:
+                continue
+
+            if menu_item.value["page"].id == self.id:
+                return menu_item.value["title"]
+
+        return self.title
+
+    class Meta:
+        abstract = True
+
+
+class MainHomePageMixin(
+    MainMenuMixin,
+    RoutablePageMixin,
+    ExtendedMetadataHomePageMixin,
+    MetadataPageMixin,
+    ArticlesMixin,
+    Page,
+):
+    # header
+
+    menu_button_name = models.CharField(
+        verbose_name="Text na tlačítku pro zapojení", max_length=16
+    )
+
+    menu_button_content = StreamField(
+        [
+            ("navbar_menu_item", NavbarMenuItemBlock()),
+        ],
+        verbose_name="Obsah menu pro zapojení se",
+        blank=True,
+        use_json_field=True,
+    )
+
+    # content
+    # NOTE: Needs to be overriden
+    content = StreamField(
+        [],
+        verbose_name="Hlavní obsah",
+        blank=True,
+        use_json_field=True,
+    )
+
+    # footer
+    footer_other_links = StreamField(
+        [
+            ("other_links", OtherLinksBlock()),
+        ],
+        verbose_name="Odkazy v zápatí webu",
+        blank=True,
+        use_json_field=True,
+    )
+
+    footer_person_list = StreamField(
+        [("person", PersonContactBlock())],
+        verbose_name="Osoby v zápatí webu",
+        blank=True,
+        max_num=6,
+        use_json_field=True,
+    )
+
+    # settings
+    @property
+    def gdpr_and_cookies_page(self):
+        # NOTE: Must be implemented
+        raise NotImplementedError
+
+    matomo_id = models.IntegerField(
+        "Matomo ID pro sledování návštěvnosti", blank=True, null=True
+    )
+
+    social_links = StreamField(
+        [
+            ("social_links", SocialLinkBlock()),
+        ],
+        verbose_name="Odkazy na sociální sítě",
+        blank=True,
+        use_json_field=True,
+    )
+
+    content_panels = Page.content_panels + [
+        FieldPanel("content"),
+        FieldPanel("footer_other_links"),
+        FieldPanel("footer_person_list"),
+    ]
+
+    promote_panels = make_promote_panels(admin_help.build(admin_help.IMPORTANT_TITLE))
+
+    settings_panels = [
+        FieldPanel("menu_button_name"),
+        FieldPanel("menu_button_content"),
+        PageChooserPanel("gdpr_and_cookies_page"),
+        FieldPanel("social_links"),
+        FieldPanel("matomo_id"),
+    ]
+
+    ### EDIT HANDLERS
+
+    edit_handler = TabbedInterface(
+        [
+            ObjectList(content_panels, heading="Obsah"),
+            ObjectList(promote_panels, heading="Propagovat"),
+            ObjectList(settings_panels, heading="Nastavení"),
+            ObjectList(MainMenuMixin.menu_panels, heading="Menu"),
+        ]
+    )
+
+    ### RELATIONS
+
+    subpage_types = []
+
+    ### OTHERS
+
+    class Meta:
+        verbose_name = "Hlavní stránka"
+        abstract = True
+
+    @property
+    def article_page_model(self):
+        # NOTE: Must be overridden
+        raise NotImplementedError
+
+    @property
+    def articles_page_model(self):
+        # NOTE: Must be overridden
+        raise NotImplementedError
+
+    @property
+    def contact_page_model(self):
+        # NOTE: Must be overridden
+        raise NotImplementedError
+
+    @property
+    def search_page_model(self):
+        # NOTE: Must be overridden
+        raise NotImplementedError
+
+    @cached_property
+    def gdpr_and_cookies_url(self):
+        if self.gdpr_and_cookies_page:
+            return self.gdpr_and_cookies_page.url
+
+        return "#"
+
+    @staticmethod
+    def get_404_response(request):
+        return render(request, "main/404.html", status=404)
+
+    def get_context(self, request, *args, **kwargs):
+        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())
+            .live()
+            .order_by("-union_date")[:3]
+        )
+
+        return context
+
+    def get_region_response(self, request):
+        context = {}
+        if request.GET.get("region", None) == "VSK":
+            sorted_article_qs = self.article_page_model.objects.filter(
+                region__isnull=False
+            ).order_by("-date")
+
+            context = {"article_data_list": sorted_article_qs[:3]}
+        else:
+            sorted_article_qs = self.article_page_model.objects.filter(
+                region=request.GET.get("region", None)
+            )[:3]
+
+            context = {"article_data_list": sorted_article_qs[:3]}
+
+        data = {
+            "html": render(
+                request, "main/includes/small_article_preview.html", context
+            ).content.decode("utf-8")
+        }
+
+        return JsonResponse(data=data, safe=False)
+
+    def serve(self, request, *args, **kwargs):
+        if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
+            if "region" in request.GET:
+                return self.get_region_response(request)
+
+        return super().serve(request, *args, **kwargs)
+
+    @cached_property
+    def newsletter_subscribe_url(self):
+        newsletter_subscribe = self.reverse_subpage("newsletter_subscribe")
+        return (
+            self.url + newsletter_subscribe
+            if self.url is not None
+            else newsletter_subscribe
+        )  # preview fix
+
+    @property
+    def articles_page(self):
+        return self._first_subpage_of_type(self.articles_page_model)
+
+    @property
+    def people_page(self):
+        return self._first_subpage_of_type(MainPeoplePage)
+
+    @property
+    def contact_page(self):
+        return self._first_subpage_of_type(self.contact_page_model)
+
+    @property
+    def search_page(self):
+        return self._first_subpage_of_type(self.search_page_model)
+
+    @property
+    def root_page(self):
+        return self
+
+    @route(r"^prihlaseni-k-newsletteru/$")
+    def newsletter_subscribe(self, request):
+        if request.method == "POST":
+            form = SubscribeForm(request.POST)
+            if form.is_valid():
+                subscribe_to_newsletter(
+                    form.cleaned_data["email"], settings.PIRATICZ_NEWSLETTER_CID
+                )
+
+                messages.success(
+                    request,
+                    "Zkontroluj si prosím schránku, poslali jsme ti potvrzovací email.",
+                )
+
+                try:
+                    page = (
+                        Page.objects.filter(id=form.cleaned_data["return_page_id"])
+                        .live()
+                        .first()
+                    )
+                    return HttpResponseRedirect(page.full_url)
+                except Page.DoesNotExist:
+                    return HttpResponseRedirect(self.url)
+
+            messages.error(
+                request,
+                "Tvůj prohlížeč nám odeslal špatná data. Prosím, zkus to znovu.",
+            )
+
+        return HttpResponseRedirect(self.url)
+
+    @route(r"^feeds/atom/$")
+    def view_feed(self, request):
+        from shared.feeds import LatestArticlesFeed  # noqa
+
+        return LatestArticlesFeed()(request, self.articles_page.id)
+
+    def _first_subpage_of_type(self, page_type) -> Page or None:
+        try:
+            return self.get_descendants().type(page_type).live().specific()[0]
+        except IndexError:
+            return None
+
+    @route(r"^sdilene/$", name="shared")
+    def shared(self, request):
+        return self.setup_article_page_context(request)
-- 
GitLab