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