Skip to content
Snippets Groups Projects
models.py 22.66 KiB
import random

from captcha.fields import CaptchaField
from django import forms
from django.core.paginator import Paginator
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,
    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 wagtailmetadata.models import MetadataPageMixin

from calendar_utils.models import CalendarMixin
from shared.blocks import ChartBlock, NewsletterSubscriptionBlock
from shared.const import RICH_TEXT_DEFAULT_FEATURES
from shared.models import (
    ArticleMixin,
    ExtendedMetadataHomePageMixin,
    ExtendedMetadataPageMixin,
    FooterMixin,
    NewsletterFormMixin,
    SubpageMixin,
)
from shared.utils import make_promote_panels, strip_all_html_tags, trim_to_length
from tuning import admin_help

from .blocks import 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,
    RICH_TEXT_FEATURES,
)


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, widget=forms.RadioSelect
    )

    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_FEATURES)
    right_text = blocks.RichTextBlock(
        label="pravý sloupec", features=RICH_TEXT_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_FEATURES)
    right_text = blocks.RichTextBlock(
        label="pravý sloupec", features=RICH_TEXT_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 PictureTitleBlock(ColorBlock):
    title = blocks.CharBlock(label="nadpis")
    picture = ImageChooserBlock(
        label="obrázek",
        help_text="rozměr na výšku 75px nebo více (obrázek bude zmenšen na výšku 75px)",
    )

    class Meta:
        label = "nadpis (s obrázkem)"
        icon = "title"
        group = "nadpisy"
        template = "uniweb/blocks/picture_title.html"


class AdvancedTextBlock(ColorBlock, AlignBlock):
    text = blocks.RichTextBlock(label="text", features=RICH_TEXT_FEATURES)

    class Meta:
        label = "text (pokročilý)"
        icon = "doc-full"
        group = "texty"
        template = "uniweb/blocks/advanced_text.html"


class PictureListBlock(ColorBlock):
    items = blocks.ListBlock(
        blocks.RichTextBlock(label="odstavec", features=RICH_TEXT_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
        context["articles"] = (
            value["page"]
            .get_children()
            .live()
            .specific()
            .order_by("-uniwebarticlepage__date")[: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",
        ],
    )

    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,
        widget=forms.RadioSelect,
    )

    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["page"]
        if page.root_page.has_calendar:
            if value["event_type"] == FUTURE:
                context["events"] = page.root_page.calendar.future_events[:count]
            else:
                context["events"] = page.root_page.calendar.past_events[:count]
        else:
            context["events"] = []
        return context


CONTENT_STREAM_BLOCKS = [
    (
        "title",
        blocks.CharBlock(
            label="nadpis",
            icon="title",
            group="nadpisy",
            template="uniweb/blocks/title.html",
        ),
    ),
    ("advanced_title", AdvancedTitleBlock()),
    ("picture_title", PictureTitleBlock()),
    (
        "text",
        blocks.RichTextBlock(
            label="text",
            features=RICH_TEXT_FEATURES,
            group="texty",
            template="uniweb/blocks/text.html",
        ),
    ),
    ("advanced_text", AdvancedTextBlock()),
    ("text_columns", ColumnsTextBlock()),
    ("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()),
    (
        "table",
        TableBlock(
            label="tabulka",
            group="ostatní",
            template="uniweb/blocks/table.html",
        ),
    ),
    ("articles", ArticlesBlock()),
    ("calendar_agenda", CalendarAgendaBlock()),
    ("chart", ChartBlock(template="uniweb/blocks/chart.html")),
]


class UniwebArticleTag(TaggedItemBase):
    content_object = ParentalKey(
        "uniweb.UniwebArticlePage",
        on_delete=models.CASCADE,
        related_name="tagged_items",
    )


class UniwebHomePage(
    Page,
    ExtendedMetadataHomePageMixin,
    MetadataPageMixin,
    CalendarMixin,
    NewsletterFormMixin,
    RoutablePageMixin,
    FooterMixin,
):
    ### FIELDS

    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,
    )
    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ů",
    )

    ### Footer

    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,
    )

    ### PANELS

    content_panels = Page.content_panels + [FieldPanel("content")]

    promote_panels = make_promote_panels(admin_help.build(admin_help.IMPORTANT_TITLE))

    settings_panels = [
        MultiFieldPanel(
            [
                FieldPanel("matomo_id"),
                FieldPanel("title_suffix"),
                FieldPanel("narrow_layout"),
            ],
            "nastavení webu",
        ),
        *NewsletterFormMixin.settings_panels,
        FieldPanel("calendar_url"),
        MultiFieldPanel(
            [
                FieldPanel("show_logo"),
                FieldPanel("show_social_links"),
                FieldPanel("show_pirate_buttons"),
                FieldPanel("footer_extra_content"),
                FieldPanel("footer_links"),
            ],
            "nastavení patičky",
        ),
    ]

    menu_panels = [FieldPanel("top_menu")]

    edit_handler = TabbedInterface(
        [
            ObjectList(content_panels, heading=gettext_lazy("Content")),
            ObjectList(promote_panels, heading=gettext_lazy("Promote")),
            ObjectList(
                settings_panels, heading=gettext_lazy("Settings"), classname="settings"
            ),
            ObjectList(menu_panels, heading="Menu"),
        ]
    )

    ### RELATIONS

    subpage_types = [
        "uniweb.UniwebFlexiblePage",
        "uniweb.UniwebArticlesIndexPage",
        "uniweb.UniwebFormPage",
        "uniweb.UniwebPeoplePage",
    ]

    ### OTHERS

    @route(r"^newsletter/$", name="newsletter")
    def newsletter(self, posted):
        return self.newsletter_post(posted)

    class Meta:
        verbose_name = "Univerzální web"

    @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,
    NewsletterFormMixin,
    RoutablePageMixin,
):
    ### 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 = NewsletterFormMixin.settings_panels

    ### RELATIONS

    parent_page_types = [
        "uniweb.UniwebHomePage",
        "uniweb.UniwebFlexiblePage",
        "uniweb.UniwebFormPage",
    ]
    subpage_types = ["uniweb.UniwebFlexiblePage", "uniweb.UniwebFormPage"]

    ### OTHERS

    @route(r"^newsletter/$", name="newsletter")
    def newsletter(self, posted):
        return self.newsletter_post(posted)

    class Meta:
        verbose_name = "Flexibilní stránka"


class UniwebArticlesIndexPage(
    Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin
):
    ### FIELDS

    ### 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ů"

    def get_context(self, request):
        context = super().get_context(request)
        num = request.GET.get("page")
        tag = request.GET.get("tag")

        articles = (
            self.get_children().live().specific().order_by("-uniwebarticlepage__date")
        )
        if tag is not None:
            articles = articles.filter(uniwebarticlepage__tags__name=tag)

        context["articles"] = Paginator(articles, ARTICLES_PER_PAGE).get_page(num)
        context["tags"] = UniwebArticleTag.tags_for(UniwebArticlePage).filter(
            uniwebarticlepage__in=articles
        )
        context["active_tag"] = tag
        return context


class UniwebArticlePage(
    ArticleMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
    ### FIELDS

    tags = ClusterTaggableManager(through=UniwebArticleTag, blank=True)

    ### PANELS

    content_panels = ArticleMixin.content_panels + [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 = ["uniweb.UniwebArticlesIndexPage"]
    subpage_types = []

    ### 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__date")[:3]
        )
        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_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é"