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 shared.models 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 . import blocks from .constants import MONTH_NAMES from .forms import JekyllImportForm from .menu import MenuMixin, PageInMenuMixin class MainHomePage( MenuMixin, 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", blocks.NavbarMenuItemBlock()), ], verbose_name="Obsah menu pro zapojení se", blank=True, use_json_field=True, ) # content content = StreamField( [ ("carousel", blocks.HomePageCarouseSlideBlock()), ("news", blocks.NewsBlock()), ("europarl_news", blocks.EuroparlNewsBlock()), ("people", blocks.PeopleOverviewBlock()), ("regions", blocks.RegionsBlock()), ("boxes", blocks.BoxesBlock()), ], verbose_name="Hlavní obsah", 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", verbose_name="Stránka pro GDPR", on_delete=models.PROTECT, blank=True, 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", "main.MainPeoplePage", "main.MainPersonPage", "main.MainSimplePage", "main.MainContactPage", "main.MainCrossroadPage", "main.MainHoaxPage", "main.MainSearchPage", ] ### OTHERS 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 articles_page(self): return self._first_subpage_of_type(MainArticlesPage) @property def people_page(self): return self._first_subpage_of_type(MainPeoplePage) @property def contact_page(self): return self._first_subpage_of_type(MainContactPage) @property def search_page(self): return self._first_subpage_of_type(MainSearchPage) @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] except IndexError: return None @route(r"^sdilene/$", name="shared") def shared(self, request): 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 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( "main.MainArticlePage", on_delete=models.CASCADE, related_name="tagged_items", ) 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, ) author_page = models.ForeignKey( "main.MainPersonPage", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Stránka autora (osoby)", ) 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 ): ### FIELDS program = StreamField( [ ("program_group", blocks.ProgramGroupBlock()), ("program_group_crossroad", blocks.ProgramGroupBlockCrossroad()), ("program_group_popout", blocks.ProgramGroupBlockPopout()), ], verbose_name="Program", blank=True, use_json_field=True, ) ### PANELS content_panels = Page.content_panels + [FieldPanel("program")] promote_panels = make_promote_panels() settings_panels = [] ### RELATIONS parent_page_types = ["main.MainHomePage"] subpage_types = [] ### OTHERS class Meta: verbose_name = "Program" class MainPeoplePage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): ### FIELDS perex_col_1 = models.TextField( verbose_name="Perex - první sloupec", ) perex_col_2 = models.TextField( verbose_name="Perex - druhý sloupec", ) people = StreamField( [ ("people_group", blocks.PeopleGroupBlock(label="Seznam osob")), ("team_group", blocks.TeamBlock()), ], verbose_name="Lidé a týmy", blank=True, use_json_field=True, ) ### PANELS content_panels = Page.content_panels + [ FieldPanel("perex_col_1"), FieldPanel("perex_col_2"), FieldPanel("people"), ] promote_panels = make_promote_panels() settings_panels = [] ### RELATIONS parent_page_types = ["main.MainHomePage"] subpage_types = [ "main.MainPersonPage", "main.MainSimplePage", ] ### OTHERS @property def perex(self) -> str: return self.perex_col_1 + " \n" + self.perex_col_2 class Meta: verbose_name = "Lidé a týmy" class MainPersonPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, CalendarMixin, PageInMenuMixin, Page, ): ### FIELDS main_image = models.ForeignKey( "wagtailimages.Image", on_delete=models.PROTECT, blank=True, null=True, verbose_name="Hlavní obrázek", related_name="+", ) profile_image = models.ForeignKey( "wagtailimages.Image", on_delete=models.PROTECT, blank=True, null=True, verbose_name="Profilový obrázek", related_name="+", ) before_name = models.CharField( "Tituly před jménem", max_length=32, blank=True, null=True ) after_name = models.CharField( "Tituly za jménem", max_length=16, blank=True, null=True ) position = models.CharField( "Pozice/povolání", max_length=200, blank=True, null=True ) primary_group = models.CharField( "Kategorie", help_text="např. 'Europarlament' nebo 'Sněmovna'", max_length=32, blank=True, null=True, ) perex = models.TextField() text = RichTextField() social_links = StreamField( [ ("social_links", blocks.SocialLinkBlock()), ], verbose_name="Odkazy na sociální sítě", blank=True, use_json_field=True, ) related_people = StreamField( [ ( "person", PageChooserBlock(page_type="main.MainPersonPage", label="Detail osoby"), ) ], verbose_name="Další lidé", blank=True, use_json_field=True, ) email = models.CharField("E-mail", max_length=128, blank=True, null=True) phone = models.CharField("Telefonní kontakt", max_length=16, blank=True, null=True) settings_panels = [] ### RELATIONS parent_page_types = ["main.MainPeoplePage"] subpage_types = [] ### PANELS content_panels = Page.content_panels + [ FieldPanel("main_image"), FieldPanel("profile_image"), FieldPanel("before_name"), FieldPanel("after_name"), FieldPanel("position"), FieldPanel("perex"), FieldPanel("text"), FieldPanel("email"), FieldPanel("phone"), FieldPanel("calendar_url"), FieldPanel("social_links"), FieldPanel("related_people"), ] def get_context(self, request) -> dict: context = super().get_context(request) context["article_page_list"] = ( MainArticlePage.objects.filter(author_page=self.id) .order_by("-date") .live()[:3] ) return context ### OTHERS class Meta: verbose_name = "Detail osoby" # ordering = ("title",) def get_background_photo(self): """ Vrací background_photo pro pozadí na stránce, pokud není nastaveno, vezme falbback z homepage """ return ( self.background_photo if self.background_photo else self.root_page.fallback_image ) class MainSimplePage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): ### FIELDS # content content = StreamField( [ ( "text", RichTextBlock(template="main/includes/atoms/text/prose_richtext.html"), ), ], verbose_name="Hlavní obsah", blank=True, use_json_field=True, ) ### PANELS content_panels = Page.content_panels + [FieldPanel("content")] promote_panels = make_promote_panels() settings_panels = [] ### RELATIONS parent_page_types = [ "main.MainHomePage", "main.MainSimplePage", "main.MainCrossroadPage", "main.MainPeoplePage", ] subpage_types = ["main.MainSimplePage"] ### OTHERS class Meta: verbose_name = "Jednoduchá stárnka" class MainContactPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): ### FIELDS contact_people = StreamField( [("item", blocks.PersonContactBlock())], verbose_name="Kontaktní osoby", blank=True, use_json_field=True, ) contact_boxes = StreamField( [("item", blocks.PersonContactBoxBlock())], verbose_name="Kontaktní boxy", blank=True, use_json_field=True, ) text = StreamField( [("two_columns_text", blocks.TwoTextColumnBlock())], verbose_name="Kontaktní informace", blank=True, use_json_field=True, ) ### PANELS content_panels = Page.content_panels + [ FieldPanel("text"), FieldPanel("contact_people"), FieldPanel("contact_boxes"), ] promote_panels = make_promote_panels() settings_panels = [] ### RELATIONS parent_page_types = ["main.MainHomePage"] subpage_types = [] ### OTHERS class Meta: verbose_name = "Kontakty" class MainCrossroadPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): ### FIELDS headlined_cards_content = StreamField( [(("headlined_cards"), blocks.CardLinkWithHeadlineBlock())], verbose_name="Karty rozcestníku s nadpisem", blank=True, use_json_field=True, ) cards_content = StreamField( [("cards", blocks.CardLinkBlock())], verbose_name="Karty rozcestníku", blank=True, use_json_field=True, ) ### PANELS content_panels = Page.content_panels + [ FieldPanel("headlined_cards_content"), FieldPanel("cards_content"), ] promote_panels = make_promote_panels() settings_panels = [] ### RELATIONS parent_page_types = [ "main.MainHomePage", "main.MainCrossroadPage", ] subpage_types = [ "main.MainSimplePage", "main.MainCrossroadPage", ] ### OTHERS class Meta: verbose_name = "Rozcestník s kartami" class MainHoaxPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): ### FIELDS description = RichTextField( "Popis", blank=True, null=True, ) content = StreamField( [(("hoax"), blocks.HoaxBlock())], verbose_name="Hoaxy a jejich vysvětlení", blank=True, use_json_field=True, ) ### PANELS content_panels = Page.content_panels + [ FieldPanel("description"), FieldPanel("content"), ] promote_panels = make_promote_panels() settings_panels = [] ### RELATIONS parent_page_types = ["main.MainHomePage"] subpage_types = [] ### OTHERS class Meta: verbose_name = "Hoaxy" class MainSearchPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): parent_page_types = ["main.MainHomePage"] subpage_types = [] class Meta: verbose_name = "Vyhledávací stránka" def get_context(self, request, *args, **kwargs): context = super().get_context(request, args, kwargs) context["results"] = [] if request.GET.get("q", "") == "": return context search_query = request.GET["q"] context["global_search_query"] = search_query for model in ( MainPersonPage, MainArticlePage, MainSimplePage, ): filter = models.Q(title__icontains=search_query) if hasattr(model, "perex"): filter = filter | models.Q(perex__icontains=search_query) results = model.objects.filter(filter) if hasattr(model, "date"): results = results.order_by("-date") context["results"] += list(results.all()[:15]) context["results"].sort(key=lambda result: result.title) return context