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.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 class MainHomePage( MenuMixin, RoutablePageMixin, ExtendedMetadataHomePageMixin, MetadataPageMixin, ArticlesMixin, Page, ): # header contact_newcomers_link = models.URLField( "URL pro zájemce o členství", blank=True, null=True, default="https://nalodeni.pirati.cz", ) contact_newcomers_text = models.TextField( "Text na tlačítku pro zájemce o členství", blank=True, null=True, default="Nalodit se", ) donation_page_link = models.URLField( "URL pro příjem darů (tlačítko Dary)", blank=True, null=True, default="https://dary.pirati.cz", ) donation_page_text = models.TextField( "Text na tlačítku pro příjem darů", blank=True, null=True, default="Darovat", ) # 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("contact_newcomers_link"), FieldPanel("contact_newcomers_text"), PageChooserPanel("gdpr_and_cookies_page"), FieldPanel("donation_page_link"), FieldPanel("donation_page_text"), 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"] = MainArticlePage.objects.live().order_by("-date")[ :3 ] articles_for_article_section = self.materialize_shared_articles_query( self.append_all_shared_articles_query(MainArticlePage.objects.all())[:8] ) context["article_main"] = ( articles_for_article_section[0] if articles_for_article_section else None ) context["article_carousel_list"] = articles_for_article_section[1:8] 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, 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__gt=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_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") .last() ) not in article_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, 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, 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, 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", "main.MainCrossroadPage", ] ### OTHERS class Meta: verbose_name = "Lidé a týmy" class MainPersonPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, CalendarMixin, 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, 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, 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, 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", "main.MainPeoplePage", ] subpage_types = [ "main.MainSimplePage", "main.MainCrossroadPage", ] ### OTHERS class Meta: verbose_name = "Rozcestník s kartami" class MainHoaxPage(ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, 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, 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 query = request.GET["q"] context["global_search_query"] = query for model in ( MainPersonPage, MainArticlePage, MainSimplePage, ): filter = models.Q(title__icontains=query) if hasattr(model, "perex"): filter = filter | models.Q(perex__icontains=query) context["results"] += list(model.objects.filter(filter).all()[:15]) print(context["results"]) context["results"].sort(key=lambda result: result.title) return context