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.blocks import ( ArticleDownloadBlock, ArticleQuoteBlock, MainMenuItemBlock, NavbarMenuItemBlock, OtherLinksBlock, PersonContactBoxBlock, SocialLinkBlock, TwoTextColumnBlock, ) from shared.const import MONTH_NAMES from shared.forms import SubscribeForm from shared.utils import make_promote_panels, subscribe_to_newsletter from tuning import admin_help from .base import ( # MenuMixin, ArticleMixin, ArticlesMixin, ArticlesPageMixin, ExtendedMetadataHomePageMixin, ExtendedMetadataPageMixin, ) from .base import MenuMixin as MenuMixinBase from .base import SharedTaggedMainArticle, SubpageMixin # MenuMixin, # MenuMixin, class MainFooterMixin(Page): footer_other_links = StreamField( [ ("other_links", OtherLinksBlock()), ], verbose_name="Odkazy v zápatí webu", blank=True, use_json_field=True, ) class Meta: abstract = True 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("menu"), ], heading="Obsah menu", ), MultiFieldPanel( [ FieldPanel("important_item_name"), FieldPanel("important_item_page"), FieldPanel("important_item_url"), ], heading="Blikající položka menu na začátku", ), ] class Meta: abstract = True class SocialMixin(Page): social_links = StreamField( [ ("social_links", SocialLinkBlock()), ], verbose_name="Odkazy na sociální sítě", blank=True, use_json_field=True, ) menu_panels = [FieldPanel("social_links")] 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, SocialMixin, RoutablePageMixin, ExtendedMetadataHomePageMixin, MetadataPageMixin, ArticlesMixin, MainFooterMixin, 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 # NOTE: Needs to be oberriden footer_person_list = StreamField( [], 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 ) 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)) menu_panels = ( MainMenuMixin.menu_panels + SocialMixin.menu_panels + [ FieldPanel("menu_button_name"), FieldPanel("menu_button_content"), ] ) settings_panels = [ PageChooserPanel("gdpr_and_cookies_page"), FieldPanel("matomo_id"), ] ### EDIT HANDLERS edit_handler = TabbedInterface( [ ObjectList(content_panels, heading="Obsah"), ObjectList(promote_panels, heading="Propagovat"), ObjectList(settings_panels, heading="Nastavení"), ObjectList(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, "styleguide2/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() ).order_by("-union_timestamp")[: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("-timestamp") 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, "styleguide2/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(self.articles_page_model, self.article_page_model)( 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 MainArticlesPageMixin( RoutablePageMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, ArticlesPageMixin, PageInMenuMixin, Page, ): last_import_log = models.TextField( "Výstup z posledního importu", null=True, blank=True ) perex = models.TextField() import_panels = [ MultiFieldPanel( [ FieldPanel("do_import"), FieldPanel("collection"), FieldPanel("dry_run"), FieldPanel("jekyll_repo_url"), FieldPanel("readonly_log"), HelpPanel( "Import provádějte vždy až po vytvoření stránky aktualit. " 'Pro uložení logu je nutné volit možnost "Publikovat", nikoliv' 'pouze "Uložit koncept". ' "Import proběhne na pozadí a může trvat až několik minut. " "Dejte si po spuštění importu kávu a potom obnovte stránku pro " "zobrazení výsledku importu." ), ], "import z Jekyll repozitáře", ), ] ### RELATIONS parent_page_types = [] # NOTE: Must be implemented subpage_types = [] # NOTE: Must be implemented ### PANELS content_panels = Page.content_panels + [ FieldPanel("perex"), FieldPanel("shared_tags"), ] promote_panels = make_promote_panels() ### EDIT HANDLERS edit_handler = TabbedInterface( [ ObjectList(content_panels, heading="Obsah"), ObjectList(promote_panels, heading="Propagovat"), ObjectList(import_panels, heading="Import"), ] ) ### OTHERS ARTICLE_LIST_COUNT = 20 class Meta: verbose_name = "Rozcestník článků" abstract = True def get_base_shared_articles_query(self, filter: models.Q): return self.materialize_shared_articles_query( self.append_all_shared_articles_query( self.root_page.article_page_model.objects.filter(filter).live().all() ).order_by("-union_timestamp") ) 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) articles = self.get_base_shared_articles_query(search_filter)[ : self.ARTICLE_LIST_COUNT + 1 ] more_articles_exist = len(articles) > self.ARTICLE_LIST_COUNT articles = articles[: self.ARTICLE_LIST_COUNT] ctx["articles"] = articles ctx["show_next_timeline_articles"] = more_articles_exist ctx["article_count"] = len(articles) tags = [] tag_count = {} for article in self.root_page.article_page_model.objects.child_of( self ).all()[:150]: for tag in article.tags.all(): if tag not in tags: tag_count[tag] = 1 if tag in tags: tag_count[tag] += 1 continue tags.append(tag) tags.sort(key=lambda tag: tag_count[tag], reverse=True) # Limit to a maximum of 30 tags tags = tags[:30] ctx["tags"] = tags # meow return ctx def get_timeline_articles_response(self, request): article_count = request.GET.get("article_count", "") search_filter = self.get_search_filters(request) if article_count.isnumeric(): article_count = int(article_count) else: article_count = self.ARTICLE_LIST_COUNT articles = self.get_base_shared_articles_query(search_filter)[ : article_count + 1 ] more_articles_exist = len(articles) > article_count articles = articles[:article_count] context = {"articles": articles} data = { "html": render( request, "styleguide2/includes/organisms/articles/articles_timeline_list.html", context, ).content.decode("utf-8"), "has_next": more_articles_exist, } 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 "article_count" in request.GET: return self.get_timeline_articles_response(request) return super().serve(request, *args, **kwargs) class MainArticlePageMixin( ArticleMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page, ): ### FIELDS show_initial_image = models.BooleanField( verbose_name="Ukázat obrázek v textu", help_text="Pokud je tato volba zaškrtnutá, obrázek nastavený u tohoto článku se automaticky vloží do prvního odstavce.", default=True, ) content = StreamField( [ ( "text", RichTextBlock( template="styleguide2/includes/atoms/text/prose_richtext.html" ), ), ("quote", ArticleQuoteBlock()), ("download", ArticleDownloadBlock()), ], verbose_name="Článek", blank=True, use_json_field=True, ) @property def tags(self): # NOTE: Must be implemented raise NotImplementedError @property def shared_tags(self): # NOTE: Must be implemented raise NotImplementedError search_fields = ArticleMixin.search_fields + [ index.SearchField("author_page"), index.FilterField("slug"), ] ### PANELS content_panels = ArticleMixin.content_panels + [ FieldPanel("author_page"), FieldPanel("show_initial_image"), FieldPanel("tags"), FieldPanel("shared_tags"), ] promote_panels = make_promote_panels( admin_help.build(admin_help.NO_SEO_TITLE, admin_help.NO_DESCRIPTION_USE_PEREX), search_image=False, ) ### RELATIONS parent_page_types = [] # NOTE: Must be implemented subpage_types = [] # NOTE: Must be implemented ### OTHERS class Meta: verbose_name = "Aktualita" abstract = True class MainContactPageMixin( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): ### FIELDS contact_people = StreamField( [], verbose_name="Kontaktní osoby", blank=True, use_json_field=True, ) contact_boxes = StreamField( [("item", PersonContactBoxBlock())], verbose_name="Kontaktní boxy", blank=True, use_json_field=True, ) text = StreamField( [("two_columns_text", 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 = [] # NOTE: Must be implemented subpage_types = [] # NOTE: Must be implemented ### OTHERS class Meta: verbose_name = "Kontakty" abstract = True class MainSearchPageMixin( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): parent_page_types = [] # NOTE: Must be implemented subpage_types = [] # NOTE: Must be implemented class Meta: verbose_name = "Vyhledávací stránka" abstract = True @property def searchable_models(self) -> list: # NOTE: Must be implemented return [] 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 self.searchable_models: 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, "timestamp"): results = results.order_by("-timestamp") context["results"] += list(results.all()[:15]) context["results"].sort( # Put results without a timestamp first, as they'll be person litsings etc. key=lambda result: result.timestamp if hasattr(result, "timestamp") else datetime.datetime(year=9999, month=1, day=1).replace( tzinfo=datetime.timezone.utc ), reverse=True, ) return context class MainSimplePageMixin( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page ): ### FIELDS # content content = StreamField( [ ( "text", RichTextBlock( template="styleguide2/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 = [] # NOTE: Must be implemented subpage_types = [] # NOTE: Must be implemented ### OTHERS class Meta: verbose_name = "Jednoduchá stárnka" abstract = True class MainPeoplePageMixin( 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", ) ### 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" abstract = True class MainPersonPageMixin( 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( "Funkce", max_length=200, blank=True, null=True, help_text="Např. 'Předseda'" ) primary_group = models.CharField( "Kategorie", help_text="např. 'Europarlament' nebo 'Sněmovna'", max_length=64, blank=True, null=True, ) perex = models.TextField() text = RichTextField() social_links = StreamField( [ ("social_links", 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 # NOTE: Must be overridden parent_page_types = [] subpage_types = [] ### PANELS content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel("main_image"), FieldPanel("profile_image"), ], heading="Obrázky", ), MultiFieldPanel( [FieldPanel("before_name"), FieldPanel("after_name")], heading="Titul", ), MultiFieldPanel( [FieldPanel("position"), FieldPanel("perex"), FieldPanel("text")], heading="Informace", ), MultiFieldPanel( [ FieldPanel("email"), FieldPanel("phone"), FieldPanel("social_links"), ], heading="Kontakt", ), FieldPanel("calendar_url"), FieldPanel("related_people"), ] def get_context(self, request) -> dict: context = super().get_context(request) context["article_page_list"] = ( self.root_page.article_page_model.objects.filter(author_page=self.id) .order_by("-timestamp") .live()[:3] ) return context ### OTHERS class Meta: verbose_name = "Detail osoby" abstract = True # ordering = ("title",)