import random from captcha.fields import CaptchaField from django.db import models from django.utils.translation import gettext_lazy from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey from taggit.models import TaggedItemBase from wagtail import blocks from wagtail.admin.panels import ( FieldPanel, InlinePanel, MultiFieldPanel, ObjectList, PageChooserPanel, TabbedInterface, ) from wagtail.contrib.forms.models import AbstractForm, AbstractFormField from wagtail.contrib.forms.panels import FormSubmissionsPanel from wagtail.contrib.routable_page.models import RoutablePageMixin, route from wagtail.contrib.table_block.blocks import TableBlock from wagtail.fields import RichTextField, StreamField from wagtail.images.blocks import ImageChooserBlock 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 ( AdvancedTextBlock, ChartBlock, GalleryBlock, HeadlineBlock, NewsletterSubscriptionBlock, PictureHeadlineBlock, ) from shared.const import RICH_TEXT_DEFAULT_FEATURES from shared.models import ( ArticleMixin, ArticlesPageMixin, CalendarMixin, ExtendedMetadataPageMixin, MainHomePageMixin, MainMenuMixin, MainSearchPageMixin, PdfPageMixin, SharedTaggedUniwebArticle, SocialMixin, SubpageMixin, ) from shared_legacy.blocks import FlipCardsBlock from shared_legacy.models import FooterMixin as LegacyFooterMixin from shared_legacy.utils import make_promote_panels, strip_all_html_tags, trim_to_length from tuning import admin_help from .blocks import AlignedTableBlock, PeopleGroupListBlock, PersonUrlBlock from .constants import ( ALIGN_CHOICES, ALIGN_CSS, ARTICLES_PER_LINE, ARTICLES_PER_PAGE, BLACK_ON_WHITE, CALENDAR_EVENTS_CHOICES, COLOR_CHOICES, COLOR_CSS, FUTURE, LEFT, ) class ColorBlock(blocks.StructBlock): """ Intended as parent class for blocks with color option. """ color = blocks.ChoiceBlock(COLOR_CHOICES, label="barva", default=BLACK_ON_WHITE) def get_context(self, value, parent_context=None): context = super().get_context(value, parent_context=parent_context) if "css_class" not in context: context["css_class"] = [] context["css_class"] += COLOR_CSS[value["color"]] return context class AlignBlock(blocks.StructBlock): """ Intended as parent class for blocks with align option. """ align = blocks.ChoiceBlock(ALIGN_CHOICES, label="zarovnání", default=LEFT) def get_context(self, value, parent_context=None): context = super().get_context(value, parent_context=parent_context) if "css_class" not in context: context["css_class"] = [] context["css_class"] += ALIGN_CSS[value["align"]] return context class ColumnsTextBlock(blocks.StructBlock): left_text = blocks.RichTextBlock( label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES ) right_text = blocks.RichTextBlock( label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES ) class Meta: label = "text dva sloupce" icon = "doc-full" group = "texty" template = "uniweb/blocks/text_columns.html" class AdvancedColumnsTextBlock(ColorBlock, AlignBlock): left_text = blocks.RichTextBlock( label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES ) right_text = blocks.RichTextBlock( label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES ) class Meta: label = "text dva sloupce (pokročilý)" icon = "doc-full" group = "texty" template = "uniweb/blocks/advanced_text_columns.html" class AdvancedTitleBlock(ColorBlock, AlignBlock): title = blocks.CharBlock(label="nadpis") class Meta: label = "nadpis (pokročilý)" icon = "title" group = "nadpisy" template = "uniweb/blocks/advanced_title.html" class PictureListBlock(ColorBlock): items = blocks.ListBlock( blocks.RichTextBlock(label="odstavec", features=RICH_TEXT_DEFAULT_FEATURES), label="odstavce", ) picture = ImageChooserBlock( label="obrázek", # TODO rozměry v helpu help_text="rozměr 25x25px nebo více (obrázek bude zmenšen na 25x25px)", ) class Meta: label = "seznam z obrázkovými odrážkami" icon = "list-ul" group = "texty" template = "uniweb/blocks/picture_list.html" class ArticlesBlock(blocks.StructBlock): page = blocks.PageChooserBlock( label="sekce článků", page_type=["uniweb.UniwebArticlesIndexPage"] ) lines = blocks.IntegerBlock( label="počet řádků", default=1, help_text="zobrazí se tři články na řádek", ) class Meta: label = "články" icon = "folder-open-1" group = "ostatní" template = "uniweb/blocks/articles.html" def get_context(self, value, parent_context=None): context = super().get_context(value, parent_context=parent_context) count = value["lines"] * ARTICLES_PER_LINE articles_page = value["page"] context["articles"] = articles_page.materialize_shared_articles_query( articles_page.append_all_shared_articles_query( UniwebArticlePage.objects.child_of(articles_page) )[:count] ) return context class MenuItemBlock(blocks.StructBlock): name = blocks.CharBlock(label="název") page = blocks.PageChooserBlock( label="stránka", page_type=[ "uniweb.UniwebHomePage", "uniweb.UniwebFlexiblePage", "uniweb.UniwebArticlesIndexPage", "uniweb.UniwebFormPage", "uniweb.UniwebPeoplePage", "uniweb.UniwebPersonPage", "uniweb.UniwebPdfPage", "district.DistrictPersonPage", ], ) class Meta: label = "stránka" class CalendarAgendaBlock(blocks.StructBlock): info = blocks.StaticBlock( label="volba kalendáře", admin_text="adresa kalendáře se zadává v nastavení hlavní stránky webu", ) count = blocks.IntegerBlock(label="maximum událostí k zobrazení", default=10) event_type = blocks.ChoiceBlock( CALENDAR_EVENTS_CHOICES, label="druh událostí", default=FUTURE, ) class Meta: label = "kalendář agenda" icon = "date" group = "ostatní" template = "uniweb/blocks/calendar_agenda.html" def get_context(self, value, parent_context=None): context = super().get_context(value, parent_context=parent_context) count = value["count"] page = context.get("page") if page and page.root_page.has_calendar: if value["event_type"] == FUTURE and page.root_page.calendar.future_events: context["events"] = page.root_page.calendar.future_events[:count] elif page.root_page.calendar.past_events: context["events"] = page.root_page.calendar.past_events[:count] else: context["events"] = [] else: context["events"] = [] return context class ButtonBlock(blocks.StructBlock): text = blocks.CharBlock(label="Nadpis") url = blocks.URLBlock( label="Odkaz", help_text="Pokud je odkaz vyplněný, není nutno vyplňovat stránku.", required=False, ) page = blocks.PageChooserBlock( label="Stránka", help_text="Pokud je stránka vyplněná, není nutno vyplňovat odkaz.", required=False, ) class Meta: label = "Tlačítko" icon = "link-external" group = "ostatní" template = "uniweb/blocks/button.html" CONTENT_STREAM_BLOCKS = [ # title, advanced_title ("headline", HeadlineBlock()), # picture_title ("picture_title", PictureHeadlineBlock()), # text ( "text", blocks.RichTextBlock( label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES, template="styleguide2/includes/atoms/text/prose_richtext.html", ), ), # advanced_text ("advanced_text", AdvancedTextBlock()), # text_columns ("text_columns", ColumnsTextBlock()), # advanced_text_columns # gallery ("new_gallery", GalleryBlock()), # picture_list # aligned_table ( "aligned_table", AlignedTableBlock( group="ostatní", template="styleguide2/includes/atoms/table/aligned_table.html", ), ), # table ( "table", TableBlock( label="Tabulka", group="ostatní", template="styleguide2/includes/atoms/table/table.html", ), ), # articles # calendar_agenda # button # chart ("chart", ChartBlock()), # cards ( "title", blocks.CharBlock( label="nadpis", icon="title", group="nadpisy", template="uniweb/blocks/title.html", ), ), ("advanced_title", AdvancedTitleBlock()), # ( # "text", # blocks.RichTextBlock( # label="Textový editor", # features=RICH_TEXT_DEFAULT_FEATURES, # template="styleguide2/includes/atoms/text/prose_richtext.html", # ), # ), ("advanced_text_columns", AdvancedColumnsTextBlock()), ( "gallery", blocks.ListBlock( ImageChooserBlock(label="obrázek"), label="galerie", icon="image", group="ostatní", template="uniweb/blocks/gallery.html", ), ), ("picture_list", PictureListBlock()), ("articles", ArticlesBlock()), ("calendar_agenda", CalendarAgendaBlock()), ("button", ButtonBlock()), ("cards", FlipCardsBlock(template="uniweb/blocks/flip_cards.html")), ] class UniwebArticleTag(TaggedItemBase): content_object = ParentalKey( "uniweb.UniwebArticlePage", on_delete=models.CASCADE, related_name="tagged_items", ) class UniwebHomePage(CalendarMixin, LegacyFooterMixin, MainHomePageMixin): ### FIELDS fallback_image = models.ForeignKey( "wagtailimages.Image", on_delete=models.PROTECT, blank=True, null=True, related_name="+", verbose_name="Obrázek pro pozadí stránek", help_text="Tento obrázek bude využit např. jako pozadí pro osobní stránky.", ) calendar_page = models.ForeignKey( "UniwebCalendarPage", verbose_name="Stránka s kalendářem", on_delete=models.PROTECT, null=True, blank=True, ) content = StreamField( CONTENT_STREAM_BLOCKS + [("newsletter", NewsletterSubscriptionBlock())], verbose_name="obsah stránky", blank=True, use_json_field=True, ) # settings matomo_id = models.IntegerField( "Matomo ID pro sledování návštěvnosti", blank=True, null=True, ) logo = models.ForeignKey( "wagtailimages.Image", on_delete=models.PROTECT, blank=True, null=True, verbose_name="Logo pro web", help_text="Pokud žádné nezadáte, použije se default logo pirátů", related_name="uniweb_logo_image", ) top_menu = StreamField( [("item", MenuItemBlock())], verbose_name="horní menu", blank=True, use_json_field=True, ) narrow_layout = models.BooleanField( "zúžený obsah stránky", default=False, help_text="užší stránka je vhodná pro lepší čitelnost textů", ) ### Header menu_button_name = models.CharField( verbose_name="Text na tlačítku pro zapojení", max_length=16, blank=True, null=True, ) ### Footer hide_footer = models.BooleanField( "skrýt patičku", default=False, help_text="Chcete skrýt patičku?" ) show_logo = models.BooleanField( "zobrazit logo", default=True, help_text="Zobrazit logo" ) show_social_links = models.BooleanField( "zobrazit soc. linky", default=True, help_text="Zobrazit link na sociální sítě" ) show_pirate_buttons = models.BooleanField( "zobrazit pirátská tlačítka", default=True, help_text="Zobrazit pirátská tlačítka", ) footer_extra_content = RichTextField( verbose_name="Extra obsah pod šedou patičkou", blank=True, features=RICH_TEXT_DEFAULT_FEATURES, ) donation_page = models.URLField( "URL pro příjem darů (tlačítko Darovat)", blank=True, null=True, default="https://dary.pirati.cz", ) ### PANELS settings_panels = [ FieldPanel("logo"), MultiFieldPanel( [ FieldPanel("matomo_id"), FieldPanel("title_suffix"), FieldPanel("narrow_layout"), FieldPanel("fallback_image"), ], "Obecné nastavení webu", ), MultiFieldPanel( [ FieldPanel("calendar_url"), PageChooserPanel("calendar_page"), ], "Kalendář", ), MultiFieldPanel( [ FieldPanel("hide_footer"), FieldPanel("show_logo"), FieldPanel("show_social_links"), FieldPanel("show_pirate_buttons"), FieldPanel("donation_page"), ], "Patička", ), ] menu_panels = ( MainMenuMixin.menu_panels + SocialMixin.menu_panels + [ FieldPanel("menu_button_name"), FieldPanel("menu_button_content"), ] ) edit_handler = TabbedInterface( [ ObjectList(MainHomePageMixin.content_panels, heading="Obsah"), ObjectList(menu_panels, heading="Hlavička"), ObjectList(MainHomePageMixin.footer_panels, heading="Patička"), ObjectList(MainHomePageMixin.promote_panels, heading="Propagace"), ObjectList(settings_panels, heading="Nastavení"), ] ) ### RELATIONS subpage_types = [ "uniweb.UniwebFlexiblePage", "uniweb.UniwebArticlesIndexPage", "uniweb.UniwebFormPage", "uniweb.UniwebPeoplePage", "uniweb.UniwebCalendarPage", "uniweb.UniwebSearchPage", ] ### OTHERS class Meta: verbose_name = "Univerzální web" @property def gdpr_and_cookies_page(self): from main.models import MainHomePage return MainHomePage.objects.first().gdpr_and_cookies_page @property def article_page_model(self): return UniwebArticlePage @property def articles_page_model(self): return UniwebArticlesIndexPage @property def search_page_model(self): return UniwebSearchPage @property def root_page(self): return self @property def has_calendar(self): return self.calendar_id is not None class UniwebFlexiblePage( Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, ): ### FIELDS content = StreamField( CONTENT_STREAM_BLOCKS + [("newsletter", NewsletterSubscriptionBlock())], verbose_name="obsah stránky", blank=True, use_json_field=True, ) ### PANELS promote_panels = make_promote_panels() content_panels = Page.content_panels + [ FieldPanel("content"), ] settings_panels = [] ### RELATIONS parent_page_types = [ "uniweb.UniwebHomePage", "uniweb.UniwebFlexiblePage", "uniweb.UniwebFormPage", ] subpage_types = [ "uniweb.UniwebFlexiblePage", "uniweb.UniwebFormPage", ] ### OTHERS class Meta: verbose_name = "Flexibilní stránka" class UniwebCalendarPage(SubpageMixin, MetadataPageMixin, CalendarMixin, Page): """ Page for displaying full calendar """ ### PANELS content_panels = Page.content_panels + [FieldPanel("calendar_url")] ### RELATIONS parent_page_types = [ "uniweb.UniwebHomePage", ] subpage_types = [] ### OTHERS class Meta: verbose_name = "Stránka s kalendářem" class UniwebArticlesIndexPage( RoutablePageMixin, Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, ArticlesPageMixin, ): ### FIELDS ### PANELS content_panels = ArticlesPageMixin.content_panels promote_panels = make_promote_panels() settings_panels = [] ### RELATIONS parent_page_types = ["uniweb.UniwebHomePage"] subpage_types = ["uniweb.UniwebArticlePage"] ### OTHERS class Meta: verbose_name = "Sekce článků" @route(r"^sdilene/$", name="shared") def shared(self, request): return self.setup_article_page_context(request) def get_context(self, request): context = super().get_context(request) num = request.GET.get("page", 1) tag = request.GET.get("tag") tag_params = self.filter_by_tag_name(tag) own_children = UniwebArticlePage.objects.child_of(self) articles = self.append_all_shared_articles_query( own_children if tag is None else own_children.filter(**tag_params), custom_article_query=lambda articles: articles.filter(**tag_params) if tag is not None else articles, ) context["articles"] = self.get_page_with_shared_articles( articles, ARTICLES_PER_PAGE, num, ) context["tags"] = self.search_tags_by_unioned_id_query(articles) context["active_tag"] = tag return context class UniwebArticlePage( ArticleMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page ): ### FIELDS tags = ClusterTaggableManager(through=UniwebArticleTag, blank=True) shared_tags = ClusterTaggableManager( verbose_name="Tagy pro sdílení mezi weby", through=SharedTaggedUniwebArticle, blank=True, ) search_fields = ArticleMixin.search_fields + [ index.FilterField("slug"), ] ### PANELS content_panels = ArticleMixin.content_panels + [ 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 = ["uniweb.UniwebArticlesIndexPage"] subpage_types = ["uniweb.UniwebPdfPage"] ### OTHERS class Meta: verbose_name = "Článek" def get_context(self, request): context = super().get_context(request) context["related_articles"] = ( ( self.get_siblings(inclusive=False) .live() .specific() .order_by("-uniwebarticlepage__timestamp")[:3] ) if self.shared_from is None else [] ) return context class UniwebFormField(AbstractFormField): page = ParentalKey( "UniwebFormPage", on_delete=models.CASCADE, related_name="form_fields" ) class UniwebFormPage( AbstractForm, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin ): ### FIELDS content_before = StreamField( CONTENT_STREAM_BLOCKS, verbose_name="obsah stránky před formulářem", blank=True, use_json_field=True, ) content_after = StreamField( CONTENT_STREAM_BLOCKS, verbose_name="obsah stránky za formulářem", blank=True, use_json_field=True, ) content_landing = StreamField( CONTENT_STREAM_BLOCKS, verbose_name="obsah stránky zobrazené po odeslání formuláře", blank=True, use_json_field=True, ) ### PANELS content_panels = AbstractForm.content_panels + [ FieldPanel("content_before"), InlinePanel("form_fields", label="formulář"), FieldPanel("content_after"), FieldPanel("content_landing"), ] promote_panels = make_promote_panels() submissions_panels = [FormSubmissionsPanel()] edit_handler = TabbedInterface( [ ObjectList(content_panels, heading=gettext_lazy("Content")), ObjectList(promote_panels, heading=gettext_lazy("Promote")), ObjectList(submissions_panels, heading="Data z formuláře"), ] ) ### RELATIONS parent_page_types = [ "uniweb.UniwebHomePage", "uniweb.UniwebFlexiblePage", "uniweb.UniwebFormPage", ] subpage_types = ["uniweb.UniwebFlexiblePage", "uniweb.UniwebFormPage"] ### OTHERS class Meta: verbose_name = "Formulářová stránka" def get_form_class(self): form = super().get_form_class() form.base_fields["captcha"] = CaptchaField(label="opište písmena z obrázku") return form # Don't waste time making a new mixin for this, # we'll be doing Octopus imports within a short while. class UniwebPersonPage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page ): ### FIELDS job = models.CharField( "Povolání", max_length=128, blank=True, null=True, help_text="Např. 'Informatik'", ) job_function = models.CharField( "Funkce", max_length=128, blank=True, null=True, help_text="Např. 'Předseda'" ) background_photo = models.ForeignKey( "wagtailimages.Image", on_delete=models.PROTECT, blank=True, null=True, related_name="+", verbose_name="obrázek do záhlaví", ) profile_photo = models.ForeignKey( "wagtailimages.Image", on_delete=models.PROTECT, blank=True, null=True, related_name="+", verbose_name="profilová fotka", ) text = RichTextField("text", blank=True, features=RICH_TEXT_DEFAULT_FEATURES) email = models.EmailField("Email", null=True, blank=True) show_email = models.BooleanField("Zobrazovat email na stránce?", default=True) phone = models.CharField("Telefon", max_length=16, blank=True, null=True) city = models.CharField("Město/obec", max_length=64, blank=True, null=True) age = models.IntegerField("Věk", blank=True, null=True) is_pirate = models.BooleanField("Je členem Pirátské strany?", default=True) other_party = models.CharField( "Strana", max_length=64, blank=True, null=True, help_text="Vyplňte pokud osoba není Pirát", ) other_party_logo = models.ForeignKey( "wagtailimages.Image", on_delete=models.PROTECT, blank=True, null=True, related_name="+", verbose_name="Logo strany", help_text="Vyplňte pokud osoba není Pirát", ) facebook_url = models.URLField("Odkaz na Facebook", blank=True, null=True) instagram_url = models.URLField("Odkaz na Instagram", blank=True, null=True) twitter_url = models.URLField("Odkaz na Twitter", blank=True, null=True) youtube_url = models.URLField("Odkaz na Youtube kanál", blank=True, null=True) flickr_url = models.URLField("Odkaz na Flickr", blank=True, null=True) custom_web_url = models.URLField("Odkaz na vlastní web", blank=True, null=True) other_urls = StreamField( [("other_url", PersonUrlBlock())], verbose_name="Další odkaz", blank=True, use_json_field=True, ) ### PANELS content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel("job"), FieldPanel("job_function"), ], "Základní údaje", ), MultiFieldPanel( [ FieldPanel("profile_photo"), FieldPanel("background_photo"), ], "Fotky", ), FieldPanel("text"), MultiFieldPanel( [ FieldPanel("email"), FieldPanel("show_email"), FieldPanel("phone"), FieldPanel("city"), FieldPanel("age"), FieldPanel("is_pirate"), FieldPanel("other_party"), FieldPanel("other_party_logo"), ], "Kontaktní informace", ), MultiFieldPanel( [ FieldPanel("facebook_url"), FieldPanel("instagram_url"), FieldPanel("twitter_url"), FieldPanel("youtube_url"), FieldPanel("flickr_url"), FieldPanel("custom_web_url"), FieldPanel("other_urls"), ], "Sociální sítě", ), ] settings_panels = [] ### RELATIONS parent_page_types = ["uniweb.UniwebPeoplePage"] subpage_types = [] ### 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 ) def get_job_description(self): """ Vrací povolání + funkci, s čárkou mezi nima, pokud jsou obě definovaná, jinak vrátí jednu z nich """ if self.job and self.job_function: return f"{self.job}, {self.job_function}" return self.job or self.job_function or "" def get_context(self, request): context = super().get_context(request) # Na strance detailu cloveka se vpravo zobrazuji 3 dalsi nahodne profily context["random_people"] = list( self.get_siblings(inclusive=False).live().specific() ) random.shuffle(context["random_people"]) context["random_people"] = context["random_people"][:3] return context def get_meta_image(self): return self.search_image or self.profile_photo def get_meta_description(self): if self.search_description: return self.search_description if self.text: return trim_to_length(strip_all_html_tags(self.text)) return None class UniwebPeoplePage( ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page ): ### FIELDS content = StreamField( [ ( "text", blocks.RichTextBlock( label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES ), ), ("people_group", PeopleGroupListBlock()), ], verbose_name="Obsah stránky", 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 = ["uniweb.UniwebHomePage"] subpage_types = ["uniweb.UniwebPersonPage"] ### OTHERS class Meta: verbose_name = "Lidé" class UniwebPdfPage(MetadataPageMixin, SubpageMixin, Page, PdfPageMixin): """ Single pdf page display """ ### RELATIONS parent_page_types = [ "uniweb.UniwebHomePage", "uniweb.UniwebArticlePage", ] subpage_types = [] ### PANELS content_panels = Page.content_panels + PdfPageMixin.content_panels ### OTHER class Meta: verbose_name = "PDF stránka" class UniwebSearchPage(MainSearchPageMixin): ### RELATIONS parent_page_types = ["uniweb.UniwebHomePage"]