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, PersonContactBlock, 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, 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, "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_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, "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()(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 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_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( self.root_page.article_page_model.objects.filter(search_filter).live() ) .order_by("-union_date") ) if not target_date_list: return [] target_date = target_date_list[0]["union_date"] - 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( self.root_page.article_page_model.objects.filter(search_filter).live().all() ) .order_by("union_date")[:2] # LIMIT 2 )[0] not in article_timeline_list[-1]["articles"] ) tags = [] for article in self.root_page.article_page_model.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, "styleguide2/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( self.root_page.article_page_model.objects.filter( search_filter ).live().all() ) .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 MainArticlePageMixin( ArticleMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page, ): ### FIELDS 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("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( [("item", PersonContactBlock())], 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, "date"): results = results.order_by("-date") context["results"] += list(results.all()[:15]) context["results"].sort( # Put results without a date first, as they'll be person litsings etc. key=lambda result: result.date if hasattr(result, "date") else datetime.date(year=9999, month=1, day=1), 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