-
Alexa Valentová authoredAlexa Valentová authored
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,
]