import json import logging import requests from datetime import date, timedelta from django.core.cache import cache from django.db import models from django.utils import timezone from icalevnt import icalevents from wagtail.admin.panels import FieldPanel, MultiFieldPanel, PublishingPanel from wagtail.fields import StreamField from wagtail.models import Page from instagram_utils.models import InstagramPost from shared.blocks import DEFAULT_CONTENT_BLOCKS, MenuItemBlock, MenuParentBlock logger = logging.getLogger(__name__) class SubpageMixin: """Must be used in class definition before MetadataPageMixin!""" @property def root_page(self): if not hasattr(self, "_root_page"): # vypada to hackove ale lze takto pouzit: dle dokumentace get_ancestors # vraci stranky v poradi od rootu, tedy domovska stranka je druha v poradi self._root_page = self.get_ancestors().specific()[1] return self._root_page def get_meta_image(self): return self.search_image or self.root_page.get_meta_image() class ArticleMixin(models.Model): """ Common fields for articles. Must be used in class definition before MetadataPageMixin! If you want to tag articles, add tags as `tags` field in article page model. """ ### FIELDS content = StreamField( DEFAULT_CONTENT_BLOCKS, verbose_name="Článek", blank=True, use_json_field=True, ) date = models.DateField("datum", default=timezone.now) perex = models.TextField("perex") author = models.CharField("autor", max_length=250, blank=True, null=True) image = models.ForeignKey( "wagtailimages.Image", on_delete=models.PROTECT, blank=True, null=True, verbose_name="obrázek", ) ### PANELS content_panels = Page.content_panels + [ FieldPanel("date"), FieldPanel("perex"), FieldPanel("content"), FieldPanel("author"), FieldPanel("image"), ] settings_panels = [PublishingPanel()] class Meta: abstract = True @classmethod def has_tags(cls): try: cls._meta.get_field("tags") except models.FieldDoesNotExist: return False return True def tag_filter_page(self): """Page used for filtering by tags in url like `?tag=foo`.""" return self.get_parent() def get_meta_image(self): if hasattr(self, "search_image") and self.search_image: return self.search_image return self.image def get_meta_description(self): if hasattr(self, "search_description") and self.search_description: return self.search_description return self.perex class MenuMixin(Page): menu = StreamField( [("menu_item", MenuItemBlock()), ("menu_parent", MenuParentBlock())], verbose_name="Menu", blank=True, use_json_field=True, ) menu_panels = [ MultiFieldPanel( [ FieldPanel("menu"), ], heading="Menu Options", ), ] class Meta: abstract = True class ExtendedMetadataHomePageMixin(models.Model): """Use for site home page to define metadata title suffix. Must be used in class definition before MetadataPageMixin! """ title_suffix = models.CharField( "Přípona titulku stránky", max_length=100, blank=True, null=True, help_text="Umožňuje přidat příponu k základnímu titulku stránky. Pokud " "je např. titulek stránky pojmenovaný 'Kontakt' a do přípony vyplníte " "'MS Pardubice | Piráti', výsledný titulek bude " "'Kontakt | MS Pardubice | Piráti'. Pokud příponu nevyplníte, použije " "se název webu.", ) class Meta: abstract = True def get_meta_title_suffix(self): if self.title_suffix: return self.title_suffix if hasattr(super(), "get_meta_title"): return super().get_meta_title() return self.get_site().site_name def get_meta_title(self): title = super().get_meta_title() suffix = self.get_meta_title_suffix() # Covers scenario when title_suffix is not set and evaluates to super().get_meta_title() value. # Rather than having MS Pardubice | MS Pardubice, just use MS Pardubice alone. if title != suffix: return f"{super().get_meta_title()} | {self.get_meta_title_suffix()}" return title class ExtendedMetadataPageMixin(models.Model): """Use for pages except for home page to use shared metadata title suffix. There are few rules on how to use this: - Do not forget to list ExtendedMetadataHomePageMixin among ancestors of the related HomePage class. - Must be used in class definition before MetadataPageMixin. - Expects SubpageMixin or equivalent exposing `root_page` property to be used for the page too. """ class Meta: abstract = True def get_meta_title_suffix(self): if not hasattr(self, "root_page"): logger.warning( "Using `ExtendedMetadataPageMixin` without `SubpageMixin` for %s", repr(self), ) return None if not hasattr(self.root_page, "get_meta_title_suffix"): logger.warning( "Using `ExtendedMetadataPageMixin` without `ExtendedMetadataHomePageMixin` on the root page for %s", repr(self), ) return None return self.root_page.get_meta_title_suffix() def get_meta_title(self): suffix = self.get_meta_title_suffix() if not suffix: return super().get_meta_title() return f"{super().get_meta_title()} | {self.get_meta_title_suffix()}" class PersonCalendarMixin(models.Model): ical_calendar_url = models.URLField( "iCal adresa kalendáře", max_length=256, blank=True, null=True, help_text=( "Podporuje Mrak, Google Kalendář a další. Návod na synchronizaci najdeš " "na pi2.cz/kalendare" ), ) def get_context(self, request) -> dict: context = super().get_context(request) if self.ical_calendar_url: context["calendar_data"] = self.get_ical_data() return context def get_ical_data(self) -> list: ical_response = cache.get(f"calendar_{self.ical_calendar_url}") if ical_response is None: ical_response = requests.get(self.ical_calendar_url) ical_response.raise_for_status() ical_response = ical_response.text cache.set( f"calendar_{self.ical_calendar_url}", ical_response, timeout=3600, # 1 hour ) parsed_events = icalevents.parse_events( ical_response, start=date.today() - timedelta(days=30), end=date.today() + timedelta(days=60), ) calendar_format_events = [] for event in parsed_events: parsed_event = { "allDay": event.all_day, "start": event.start.isoformat(), "end": event.end.isoformat(), } if event.summary is not None: parsed_event["title"] = event.summary if event.url is not None: parsed_event["url"] = event.url calendar_format_events.append(parsed_event) return json.dumps(calendar_format_events) class Meta: abstract = True