from functools import cached_property from dateutil.relativedelta import relativedelta from django.conf import settings 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 TaggedItemBase from wagtail.admin.edit_handlers import ( FieldPanel, HelpPanel, MultiFieldPanel, ObjectList, TabbedInterface, ) from wagtail.admin.panels import PageChooserPanel from wagtail.contrib.routable_page.models import RoutablePageMixin, route from wagtail.core.blocks import CharBlock, RichTextBlock from wagtail.core.fields import RichTextField, StreamField from wagtail.core.models import Page from wagtailmetadata.models import MetadataPageMixin from elections2021.constants import REGION_CHOICES # pozor, import ze sousedního modulu from shared.forms import SubscribeForm from shared.models import ( # MenuMixin, ArticleMixin, ExtendedMetadataHomePageMixin, ExtendedMetadataPageMixin, SubpageMixin, ) from shared.utils import make_promote_panels, subscribe_to_newsletter from tuning import admin_help from twitter_utils.models import Tweet from . import blocks from .constants import MONTH_NAMES from .forms import JekyllImportForm from .menu import MenuMixin class ARTICLE_TYPES(models.IntegerChoices): WORK_TIMELINE = 1, "Článek na timeline Piráti pracují" PRESS_RELEASE = 2, "Tisková zpráva" class MainHomePage( MenuMixin, RoutablePageMixin, ExtendedMetadataHomePageMixin, MetadataPageMixin, 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.HomePageCarouselBlock()), ("news", blocks.NewsBlock()), ("people", blocks.PeopleOverviewBlock()), ("regions", blocks.RegionsBlock()), ("tweets", blocks.TweetsBlock()), ("boxes", blocks.BoxesBlock()), ], verbose_name="Hlavní obsah", blank=True, ) # footer footer_other_links = StreamField( [ ("other_links", blocks.OtherLinksBlock()), ], verbose_name="Bloky dalších odkazů v zápatí webu", blank=True, ) footer_person_list = StreamField( [("person", blocks.PersonContactBlock())], verbose_name="Osoby v zápatí webu", blank=True, max_num=6, ) # 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ě v zápatí webu", blank=True, ) twitter_usernames = StreamField( [("username", CharBlock(label="Twitter uživatelské jméno"))], verbose_name="Uživatelská jména pro synchronizované twitter účty", blank=True, max_num=64, ) 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"), FieldPanel("twitter_usernames"), ] ### 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", ] ### 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) twitter_username_list = [ username_data["value"] for username_data in self.twitter_usernames.raw_data ] tweet_list = Tweet.objects.username_list(twitter_username_list).order_by( "-twitter_id" ) context["tweet_list"] = tweet_list[:4] context["show_next_tweet"] = len(tweet_list) > 4 context["regions"] = REGION_CHOICES context["article_data_list"] = MainArticlePage.objects.filter( region__isnull=False ).order_by("-date")[:3] articles_for_article_section = MainArticlePage.objects.filter( article_type=ARTICLE_TYPES.PRESS_RELEASE ).order_by("-date") 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): if request.GET.get("region", None) == "VSK": sorted_article_qs = MainArticlePage.objects.filter( region__isnull=False ).order_by("-date") else: sorted_article_qs = MainArticlePage.objects.filter( region=request.GET.get("region", None) ).order_by("-date") 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 get_twitter_response(self, request): twitter_username_list = [ username_data["value"] for username_data in self.twitter_usernames.raw_data ] tweet_qs = Tweet.objects.username_list(twitter_username_list).order_by( "-twitter_id" ) tweet_paginator = Paginator(tweet_qs, 4) tweet_page = tweet_paginator.get_page(request.GET.get("page", 1)) context = {"tweet_list": tweet_page.object_list} html_content = render( request, "main/includes/twitter_widget.html", context ).content data = { "html": html_content.decode("utf-8"), "has_next": tweet_page.has_next(), } 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) else: return self.get_twitter_response(request) return super().serve(request, *args, **kwargs) @cached_property def newsletter_subscribe_url(self): return self.url + self.reverse_subpage("newsletter_subscribe") @property def articles_page(self): return self._first_subpage_of_type(MainArticlesPage) @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_ID, settings.PIRATICZ_NEWSLETTER_SOURCE, ) 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) return HttpResponseRedirect(self.url) 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 class MainArticlesPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page ): perex = models.TextField() last_import_log = models.TextField( "Výstup z posledního importu", null=True, blank=True ) 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")] 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_article_data_list(self, months_back: int = 1): target_date_list = ( MainArticlePage.objects.filter(article_type=ARTICLE_TYPES.WORK_TIMELINE) .order_by("-date") .values_list("date", flat=True) ) if not target_date_list: return [self.get_empty_month_data(timezone.now().date())] target_date = target_date_list[0] - relativedelta(months=months_back) first_day_of_target_month = target_date.replace(day=1) sorted_article_qs = MainArticlePage.objects.filter( date__gt=first_day_of_target_month, article_type=ARTICLE_TYPES.WORK_TIMELINE ).order_by("-date") article_data_list = [] current_month_data = self.get_empty_month_data(timezone.now().date()) article_counter = 1 for article in sorted_article_qs: if article.date.month != current_month_data["month_number"]: article_data_list.append(current_month_data) # append completed month current_month_data = self.get_empty_month_data(article.date) article_counter = 1 current_column = "left_column" if article_counter % 2 else "right_column" current_month_data[current_column].append(article) article_counter += 1 article_data_list.append(current_month_data) # last iteration return article_data_list def get_context(self, request, *args, **kwargs): ctx = super().get_context(request, args, kwargs) article_timeline_list = self.get_article_data_list(1) ctx["article_timeline_list"] = article_timeline_list ctx["show_next_timeline_articles"] = MainArticlePage.objects.filter( article_type=ARTICLE_TYPES.WORK_TIMELINE ).count() > len(article_timeline_list) article_list = MainArticlePage.objects.filter( article_type=ARTICLE_TYPES.PRESS_RELEASE ).order_by("-date")[ :11 ] # dám LIMIT +1, abych věděl, jestli má cenu show_next ctx["article_article_list"] = article_list[:10] ctx["show_next_article"] = len(article_list) > 10 return ctx def get_timeline_articles_response(self, request): article_list = self.get_article_data_list(int(request.GET.get("months", None))) context = {"article_data_list": article_list} data = { "html": render( request, "main/blocks/articles_timeline_block.html", context ).content.decode("utf-8"), "last_article": article_list[-1] == MainArticlePage.objects.filter(article_type=ARTICLE_TYPES.WORK_TIMELINE) .order_by("-date") .last(), } return JsonResponse(data=data, safe=False) def get_articles_response(self, request): article_paginator = Paginator( MainArticlePage.objects.filter( article_type=ARTICLE_TYPES.PRESS_RELEASE ).order_by("-date", "title"), 10, ) article_page = article_paginator.get_page(request.GET.get("page", 1)) context = {"article_data_list": article_page.object_list} html_content = render( request, "main/includes/person_article_preview.html", context ).content data = { "html": html_content.decode("utf-8"), "has_next": article_page.has_next(), } return JsonResponse(data=data, safe=False) 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) if "page" in request.GET: return self.get_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], "left_column": [], "right_column": [], } class MainArticleTag(TaggedItemBase): content_object = ParentalKey("main.MainArticlePage", on_delete=models.CASCADE) class MainArticlePage( ArticleMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page ): ### FIELDS article_type = models.PositiveSmallIntegerField( "Typ článku", choices=ARTICLE_TYPES.choices, default=ARTICLE_TYPES.PRESS_RELEASE ) is_black = models.BooleanField("Má tmavé pozadí?", default=False) content = StreamField( [ ("text", RichTextBlock(template="main/blocks/rich_text_block.html")), ("quote", blocks.ArticleQuoteBlock()), ("download", blocks.ArticleDownloadBlock()), ("image", blocks.ArticleImageBlock()), ], verbose_name="Článek", blank=True, ) author_page = models.ForeignKey( "main.MainPersonPage", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Stránka autora (osoby)", ) region = models.CharField( choices=REGION_CHOICES, null=True, blank=True, max_length=3, verbose_name="Kraj", help_text="Kraj, ke kterému se článek vztahuje", ) tags = ClusterTaggableManager(through=MainArticleTag, blank=True) ### PANELS content_panels = ArticleMixin.content_panels + [ FieldPanel("article_type"), FieldPanel("author_page"), FieldPanel("is_black"), FieldPanel("region"), FieldPanel("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 perex = RichTextField() program = StreamField( [("program_group", blocks.ProgramGroupBlock(label="Část programu"))], verbose_name="Program", blank=True, ) ### PANELS content_panels = Page.content_panels + [FieldPanel("perex"), 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 = RichTextField() people = StreamField( [("people_group", blocks.PeopleGroupBlock(label="Seznam osob"))], verbose_name="Lidé", blank=True, ) ### PANELS content_panels = Page.content_panels + [FieldPanel("perex"), FieldPanel("people")] promote_panels = make_promote_panels() settings_panels = [] ### RELATIONS parent_page_types = ["main.MainHomePage"] subpage_types = ["main.MainPersonPage"] ### OTHERS class Meta: verbose_name = "Lidé" class MainPersonPage(ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, 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 ) perex = models.TextField() text = RichTextField() twitter_username = models.CharField( "Uživatelské jméno twitter pro získání příspěvků", blank=True, null=True, max_length=32, help_text="Uživatelské jméno zadejte bez @ na začátku", ) social_links = StreamField( [ ("social_links", blocks.SocialLinkBlock()), ], verbose_name="Odkazy na sociální sítě", blank=True, ) people = StreamField( [("people_group", blocks.PeopleGroupBlock(label="Seznam osob"))], verbose_name="Další lidé", blank=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("twitter_username"), FieldPanel("text"), FieldPanel("email"), FieldPanel("phone"), FieldPanel("social_links"), FieldPanel("people"), ] def get_context(self, request): context = super().get_context(request) context["article_page_list"] = MainArticlePage.objects.filter( author_page=self.id ) context["tweet_list"] = Tweet.objects.username(self.twitter_username).order_by( "-twitter_id" )[:20] 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/blocks/rich_text_block.html")), ], verbose_name="Hlavní obsah", blank=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", ] 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, ) contact_boxes = StreamField( [("item", blocks.PersonContactBoxBlock())], verbose_name="Kontaktní boxy", blank=True, ) text = StreamField( [("two_columns_text", blocks.TwoTextColumnBlock())], verbose_name="Kontaktní informace", blank=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, ) cards_content = StreamField( [("cards", blocks.CardLinkBlock())], verbose_name="Karty rozcestníku", blank=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"