Skip to content
Snippets Groups Projects
models.py 20.94 KiB
from datetime import date, datetime

from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.core.mail import EmailMessage
from django.db import models
from django.shortcuts import render
from django_ratelimit.core import is_ratelimited
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from taggit.models import TaggedItemBase
from wagtail.admin.panels import (
    FieldPanel,
    HelpPanel,
    MultiFieldPanel,
    ObjectList,
    TabbedInterface,
)
from wagtail.blocks import RichTextBlock
from wagtail.contrib.routable_page.models import route
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page
from wagtailmetadata.models import MetadataPageMixin

from shared import blocks as shared_blocks
from shared.const import RICH_TEXT_DEFAULT_FEATURES
from shared.models import (  # MenuMixin,
    ArticleMixin,
    ExtendedMetadataHomePageMixin,
    ExtendedMetadataPageMixin,
    MainArticlePageMixin,
    MainArticlesPageMixin,
    MainContactPageMixin,
    MainHomePageMixin,
    MainPeoplePageMixin,
    MainPersonPageMixin,
    MainProgramPageMixin,
    MainSearchPageMixin,
    MainSimplePageMixin,
    PageInMenuMixin,
    SharedTaggedMainArticle,
    SubpageMixin,
)
from shared.utils import make_promote_panels

from . import blocks
from .forms import CareerSubmissionForm, MainArticlesPageForm


class MainHomePage(MainHomePageMixin):
    # menu

    popout_button_name = models.CharField(
        "Název vyskakovacího tlačítka",
        max_length=16,
        blank=True,
        null=True,
    )

    popout_button_content = StreamField(
        [
            ("navbar_menu_item", shared_blocks.NavbarMenuItemBlock()),
        ],
        verbose_name="Obsah vyskakovacího tlačítka",
        blank=True,
        use_json_field=True,
    )

    ecomail_newsletter_list_id = models.IntegerField(
        "ID Ecomail newsletteru",
        blank=True,
        null=True,
    )

    ecomail_newsletter_list_tags = models.CharField(
        "Tagy k přidání novým odběratelům na Ecomailu",
        max_length=128,
        blank=True,
        null=True,
        help_text="Oddělte čárkou, například 'Tag1,Tag2,Tag3'. Bez mezer.",
    )

    # content
    content = StreamField(
        [
            ("carousel", blocks.HomePageCarouseSlideBlock()),
            (
                "news",
                shared_blocks.NewsBlock(
                    template="styleguide2/includes/organisms/articles/articles_section.html"
                ),
            ),
            # ("europarl_news", blocks.EuroparlNewsBlock()),
            ("people", shared_blocks.PeopleOverviewBlock()),
            ("regions", blocks.RegionsBlock()),
            ("boxes", blocks.BoxesBlock()),
        ],
        verbose_name="Hlavní obsah",
        blank=True,
        use_json_field=True,
    )

    # footer
    footer_person_list = StreamField(
        [("person", blocks.PersonContactBlock())],
        verbose_name="Osoby v zápatí webu",
        blank=True,
        max_num=6,
        use_json_field=True,
    )

    # settings
    gdpr_and_cookies_page = models.ForeignKey(
        "main.MainSimplePage",
        verbose_name="Stránka pro GDPR",
        on_delete=models.PROTECT,
        blank=True,
        null=True,
    )

    subpage_types = [
        "main.MainArticlesPage",
        "main.MainProgramPage",
        "main.MainPeoplePage",
        "main.MainPersonPage",
        "main.MainSimplePage",
        "main.MainContactPage",
        "main.MainCrossroadPage",
        "main.MainHoaxPage",
        "main.MainSearchPage",
        "main.MainResultsPage",
        "main.MainCareersPage",
    ]

    ### PANELS

    menu_panels = MainHomePageMixin.menu_panels + [
        FieldPanel("popout_button_name"),
        FieldPanel("popout_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.settings_panels
                + [
                    FieldPanel("ecomail_newsletter_list_id"),
                    FieldPanel("ecomail_newsletter_list_tags"),
                ],
                heading="Nastavení",
            ),
            ObjectList(MainHomePageMixin.promote_panels, heading="Metadata"),
        ]
    )

    ### OTHERS

    class Meta:
        verbose_name = "HomePage pirati.cz"

    @property
    def careers_page(self):
        return self._first_subpage_of_type(MainCareersPage)

    @property
    def article_page_model(self):
        return MainArticlePage

    @property
    def articles_page_model(self):
        return MainArticlesPage

    @property
    def contact_page_model(self):
        return MainContactPage

    @property
    def search_page_model(self):
        return MainSearchPage

    @property
    def people_page_model(self):
        return MainPeoplePage

    @property
    def root_page(self):
        return self

    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 MainArticleTag(TaggedItemBase):
    content_object = ParentalKey(
        "main.MainArticlePage",
        on_delete=models.CASCADE,
        related_name="main_tagged_items",
    )


class MainArticlesPage(MainArticlesPageMixin):
    base_form_class = MainArticlesPageForm

    displayed_tags = ParentalManyToManyField(
        "main.MainArticleTag",
        verbose_name="Z tohoto webu",
        related_name="+",
        blank=True,
    )

    displayed_shared_tags = ParentalManyToManyField(
        "shared.SharedTag",
        verbose_name="Sdílecí",
        related_name="+",
        blank=True,
    )

    parent_page_types = ["main.MainHomePage"]
    subpage_types = ["main.MainArticlePage"]


class MainArticlePage(MainArticlePageMixin):
    author_page = models.ForeignKey(
        "main.MainPersonPage",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name="Stránka autora (osoby)",
    )
    tags = ClusterTaggableManager(
        through=MainArticleTag, related_name="main_tagged_articles", blank=True
    )
    shared_tags = ClusterTaggableManager(
        verbose_name="Štítky pro sdílení mezi weby",
        through=SharedTaggedMainArticle,
        blank=True,
    )

    parent_page_types = ["main.MainArticlesPage"]
    subpage_types = []


class MainProgramPage(MainProgramPageMixin):
    ### FIELDS

    program = StreamField(
        [
            ("program_group", shared_blocks.ProgramGroupBlock()),
            ("program_group_crossroad", blocks.ProgramGroupBlockCrossroad()),
            ("program_group_popout", blocks.ProgramGroupBlockPopout()),
            ("program_group_with_candidates", blocks.ProgramGroupWithCandidatesBlock()),
            ("elections_program", blocks.ElectionsProgramBlock()),
        ],
        verbose_name="Programy",
        blank=True,
        use_json_field=True,
    )

    ### RELATIONS

    parent_page_types = ["main.MainHomePage"]
    subpage_types = []


class MainPeoplePage(MainPeoplePageMixin):
    content = StreamField(
        [
            ("people_group", blocks.PeopleGroupBlock(label="Seznam osob")),
            ("team_group", blocks.TeamBlock()),
        ],
        verbose_name="Lidé a týmy",
        blank=True,
        use_json_field=True,
    )

    parent_page_types = ["main.MainHomePage"]
    subpage_types = [
        "main.MainPersonPage",
        "main.MainSimplePage",
    ]


class MainPersonPage(MainPersonPageMixin):
    ### RELATIONS

    parent_page_types = ["main.MainPeoplePage"]


class MainSimplePage(MainSimplePageMixin):
    parent_page_types = [
        "main.MainHomePage",
        "main.MainSimplePage",
        "main.MainCrossroadPage",
        "main.MainPeoplePage",
    ]
    subpage_types = ["main.MainSimplePage"]


class MainContactPage(MainContactPageMixin):
    ### FIELDS

    contact_people = StreamField(
        [("item", blocks.PersonContactBlock())],
        verbose_name="Kontaktní osoby",
        blank=True,
        use_json_field=True,
    )

    ### RELATIONS

    parent_page_types = ["main.MainHomePage"]
    subpage_types = []


class MainCrossroadPage(
    ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
    ### FIELDS

    headlined_cards_content = StreamField(
        [(("headlined_cards"), blocks.CardLinkWithHeadlineBlock())],
        verbose_name="Karty rozcestníku s nadpisem",
        blank=True,
        use_json_field=True,
    )
    cards_content = StreamField(
        [("cards", blocks.CardLinkBlock())],
        verbose_name="Karty rozcestníku",
        blank=True,
        use_json_field=True,
    )

    ### PANELS

    content_panels = Page.content_panels + [
        FieldPanel("headlined_cards_content"),
        FieldPanel("cards_content"),
    ]

    promote_panels = make_promote_panels()

    settings_panels = []

    ### RELATIONS

    parent_page_types = [
        "main.MainHomePage",
        "main.MainCrossroadPage",
    ]
    subpage_types = [
        "main.MainSimplePage",
        "main.MainCrossroadPage",
    ]
    ### OTHERS

    class Meta:
        verbose_name = "Rozcestník s kartami"


class MainHoaxPage(
    ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
    ### FIELDS

    description = RichTextField(
        "Popis",
        blank=True,
        null=True,
    )

    content = StreamField(
        [(("hoax"), blocks.HoaxBlock())],
        verbose_name="Hoaxy a jejich vysvětlení",
        blank=True,
        use_json_field=True,
    )

    ### PANELS

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

    promote_panels = make_promote_panels()

    settings_panels = []

    ### RELATIONS

    parent_page_types = ["main.MainHomePage"]
    subpage_types = []

    ### OTHERS

    class Meta:
        verbose_name = "Hoaxy"


class MainResultsPage(
    ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
    ### FIELDS

    content = StreamField(
        [
            (("flip_cards"), shared_blocks.FlipCardsBlock()),
            (
                "text",
                RichTextBlock(
                    template="styleguide2/includes/atoms/text/prose_richtext.html"
                ),
            ),
        ],
        verbose_name="Obsah",
        blank=True,
        use_json_field=True,
    )

    ### PANELS

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

    parent_page_types = ["main.MainHomePage"]
    subpage_types = []

    class Meta:
        verbose_name = "Výsledky"


class MainCareersPage(
    ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
    subheading = models.CharField(
        verbose_name="Podtitulek",
        help_text="Text pod hlavním nadpisem stránky",
        max_length=32,
        blank=True,
        null=True,
    )

    perex_col_1 = models.TextField(
        verbose_name="Perex - první sloupec",
        blank=True,
        null=True,
    )
    perex_col_2 = models.TextField(
        verbose_name="Perex - druhý sloupec",
        blank=True,
        null=True,
    )

    content_panels = Page.content_panels + [
        HelpPanel(
            "Pro přidání pracovních nabídek ulož tuto stránku a přidej ji podstránky."
        ),
        FieldPanel("subheading"),
        MultiFieldPanel(
            [
                FieldPanel("perex_col_1", heading="První sloupec"),
                FieldPanel("perex_col_2", heading="Druhý sloupec"),
            ],
            "Perex",
        ),
    ]

    parent_page_types = ["main.MainHomePage"]
    subpage_types = ["main.MainCareerPage"]

    def get_context(self, request, *args, **kwargs) -> dict:
        context = super().get_context(request, *args, **kwargs)

        context["show_closed"] = request.GET.get("show_closed", "false") == "true"

        return context

    def get_career_categories(self) -> list[str]:
        return (
            MainCareerPage.objects.child_of(self)
            .live()
            .distinct("category")
            .values_list("category", flat=True)
            .order_by("category")
            .all()
        )

    def get_career_pages(self, show_closed: bool = False, category: str | None = None):
        filter = models.Q()

        current_date = date.today()

        if not show_closed:
            filter = filter & models.Q(closing_date__gte=current_date)

        if category is not None:
            filter = filter & models.Q(category=category)

        return MainCareerPage.objects.child_of(self).filter(filter).live().all()

    class Meta:
        verbose_name = "Kariéry"


class MainCareerPage(
    ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
    recipient_emails = models.CharField(
        verbose_name="Příjemci emailů o nových přihláškách",
        help_text="Zadej buď jednu adresu, nebo víc, oddělených čárkami.",
        blank=False,
        null=False,
    )

    category = models.CharField(
        verbose_name="Kategorie pracovní pozice",
        help_text="Např. 'Koordinátor/ka', 'Programátor/ka', 'Volební manažer/ka', ...",
        blank=False,
        null=False,
    )

    location = models.CharField(
        verbose_name="Místo výkonu práce",
        help_text="Např. 'Středočeský kraj'",
        max_length=64,
        blank=False,
        null=False,
    )

    time_cost = models.CharField(
        verbose_name="Časová náročnost",
        help_text="Např. '8h denně'",
        max_length=64,
        blank=False,
        null=False,
    )

    employment_relationship = models.CharField(
        verbose_name="Typ smlouvy",
        help_text="Např. 'Rámcová smlouva na dobu určitou'",
        max_length=128,
        blank=False,
        null=False,
    )

    pay_rate = models.CharField(
        verbose_name="Odměna",
        help_text="Např. '300-350 Kč/h'",
        max_length=64,
        blank=False,
        null=False,
    )

    created_date = models.DateField(
        verbose_name="Datum vytvoření", blank=False, null=False, default=date.today
    )

    submission_end_date = models.DateField(
        verbose_name="Datum konce přihlášek",
        blank=False,
        null=False,
    )

    closing_date = models.DateField(
        verbose_name="Datum uzavření",
        blank=False,
        null=False,
    )

    content = RichTextField(
        "Text nabídky", blank=True, null=True, features=RICH_TEXT_DEFAULT_FEATURES
    )

    result = RichTextField(
        "Výsledek výběrového řízení",
        blank=True,
        null=True,
        features=RICH_TEXT_DEFAULT_FEATURES,
    )

    proceedings_url = models.URLField(
        verbose_name="Odkaz na průběh výběrového řízení",
        help_text="Na Redmine, Fóru apod.",
        blank=True,
        null=True,
    )

    content_panels = Page.content_panels + [
        MultiFieldPanel(
            [
                FieldPanel("created_date"),
                FieldPanel("submission_end_date"),
                FieldPanel("closing_date"),
            ],
            "Datumy",
        ),
        FieldPanel("recipient_emails"),
        FieldPanel("category"),
        FieldPanel("location"),
        FieldPanel("time_cost"),
        FieldPanel("employment_relationship"),
        FieldPanel("pay_rate"),
        FieldPanel("proceedings_url"),
        FieldPanel("content"),
        FieldPanel("result"),
    ]

    parent_page_types = ["main.MainCareersPage"]

    def serve(self, request):
        if is_ratelimited(
            request, group="career_submissions", key="ip", rate="2/m", method="POST"
        ):
            raise PermissionDenied("Rate limit exceeded")

        form = None
        current_time = datetime.now()
        current_date = current_time.date()

        if (
            request.method == "POST"
            and self.closing_date >= current_date
            and self.submission_end_date >= current_date
        ):
            form = CareerSubmissionForm(request.POST, request.FILES)

            if form.is_valid():
                other_files_names = ""

                for file in form.cleaned_data["other_files"]:
                    other_files_names += f"        - {file.name}\n"

                recipient_email = EmailMessage(
                    # Subject
                    f"Potvrzení přihlášky k výběrovému řízení {self.title}",
                    # Message
                    f"""
Dobrý den,

potvrzujeme přijetí Vaší přihlášky k výběrovému řízení '{self.title}'.
Budeme vás co nejdříve kontaktovat s dalšími informacemi.

V případě nejasností můžete kontaktovat kancelář, kontakty lze nalézt zde: https://wiki.pirati.cz/kas/start

Děkujeme,
Pirátská strana
""",
                    # From email
                    "vyberka@pirati.cz",
                    # To email
                    [form.cleaned_data["email"]],
                )

                administrator_email = EmailMessage(
                    # Subject
                    f"Nová přihláška k výběrovému řízení {self.title} - {form.cleaned_data['name']} {form.cleaned_data['surname']}",
                    # Message
                    (
                        f"""
K výběrovému řízení {self.title} se {current_time} přihlásil nový zájemce.

Vyplněné údaje:

    Jméno: {form.cleaned_data['name']}
    Příjmení: {form.cleaned_data['surname']}
    E-mail: {form.cleaned_data['email']}
    Telefon: {form.cleaned_data['phone']}
    Chce posílat další nabídky: {'Ano' if form.cleaned_data['other_offers_agreement'] else 'Ne'}
    Vlastní text: {form.cleaned_data['own_text'] if form.cleaned_data['own_text'] else '(nevyplněn)'}

CV, motivační dopis a ostatní soubory jsou v přílohách. Názvy souborů:

    CV: {form.cleaned_data["cv_file"].name}
    Mot. dopis: {form.cleaned_data["cover_letter_file"].name}
    Ostatní soubory:
{other_files_names}

Při otevírání souborů buďte opatrní, virový sken proběhl, ale nemusí být přesný!
                        """
                    ),
                    # From email
                    "vyberka@pirati.cz",
                    # Recipient list
                    self.recipient_emails.split(","),
                )

                form.cleaned_data["cv_file"].seek(0)
                administrator_email.attach(
                    form.cleaned_data["cv_file"].name,
                    form.cleaned_data["cv_file"].read(),
                    form.cleaned_data["cv_file"].content_type,
                )

                form.cleaned_data["cover_letter_file"].seek(0)
                administrator_email.attach(
                    form.cleaned_data["cover_letter_file"].name,
                    form.cleaned_data["cover_letter_file"].read(),
                    form.cleaned_data["cover_letter_file"].content_type,
                )

                for file in form.cleaned_data["other_files"]:
                    file.seek(0)
                    administrator_email.attach(
                        file.name, file.read(), file.content_type
                    )

                sent_successfully = (
                    administrator_email.send() and recipient_email.send()
                )

                if sent_successfully:
                    messages.add_message(
                        request, messages.SUCCESS, "Přihláška odeslána."
                    )
                else:
                    messages.add_message(
                        request,
                        messages.ERROR,
                        "Odeslání přihlášky selhalo. Zkuste to znovu.",
                    )
            else:
                errors = ""

                for error_val in form.errors.values():
                    errors += f"{error_val.as_text()}\n"

                messages.add_message(
                    request,
                    messages.ERROR,
                    f"""
Odeslání přihlášky selhalo:
{errors}
""",
                )

        # Recreate form either way, since we don't want it to be prepopulated with data.
        form = CareerSubmissionForm()

        return render(
            request,
            self.template,
            {
                "page": self,
                "self": self,
                "form": form,
                "current_date": date.today(),
            },
        )

    class Meta:
        verbose_name = "Pracovní nabídka"


class MainSearchPage(MainSearchPageMixin):
    parent_page_types = ["main.MainHomePage"]
    subpage_types = []

    @property
    def searchable_models(self) -> list:
        return [
            MainArticlePage,
            MainPersonPage,
            MainSimplePage,
        ]