Select Git revision
0194_add_people_block.py
-
Alexa Valentová authoredAlexa Valentová authored
models.py 56.66 KiB
import json
import random
from functools import cached_property
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models
from django.http import HttpResponseNotFound, HttpResponseRedirect
from django.shortcuts import render
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import Tag, TaggedItemBase
from wagtail.admin.panels import (
FieldPanel,
HelpPanel,
InlinePanel,
MultiFieldPanel,
ObjectList,
PageChooserPanel,
TabbedInterface,
)
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Orderable, Page
from wagtail.search import index
from wagtailmetadata.models import MetadataPageMixin
from calendar_utils.models import CalendarMixin
from maps_utils.blocks import MapPointBlock
from maps_utils.const import (
DEFAULT_MAP_STYLE,
MAP_STYLES,
SUPPORTED_FEATURE_TYPES,
TILE_SERVER_CONFIG,
)
from maps_utils.validation import validators as maps_validators
from shared.blocks import (
DEFAULT_CONTENT_BLOCKS,
ButtonGroupBlock,
ChartBlock,
FigureBlock,
FullSizeHeaderBlock,
HeadlineBlock,
NewsletterSubscriptionBlock,
YouTubeVideoBlock,
)
from shared.const import RICH_TEXT_DEFAULT_FEATURES
from shared.models import (
ArticleMixin,
ArticlesMixin,
ArticlesPageMixin,
ExtendedMetadataHomePageMixin,
ExtendedMetadataPageMixin,
FooterMixin,
MenuMixin,
PdfPageMixin,
SharedTaggedDistrictArticle,
SubpageMixin,
)
from shared.utils import make_promote_panels, strip_all_html_tags, trim_to_length
from tuning import admin_help
from . import blocks
from .forms import JekyllImportForm
CONTENT_BLOCKS = DEFAULT_CONTENT_BLOCKS + [
("chart", ChartBlock(template="district/blocks/chart.html")),
("related", blocks.ArticlesBlock()),
("related_links", blocks.ArticleLinksBlock()),
]
class DistrictHomePage(
RoutablePageMixin,
MenuMixin,
ExtendedMetadataHomePageMixin,
MetadataPageMixin,
CalendarMixin,
FooterMixin,
ArticlesMixin,
Page,
):
### FIELDS
calendar_page = models.ForeignKey(
"DistrictCalendarPage",
verbose_name="Stránka s kalendářem",
on_delete=models.PROTECT,
null=True,
blank=True,
)
subheader = StreamField(
[
("header_full_size", FullSizeHeaderBlock()),
("header_simple", blocks.HomepageSimpleHeaderBlock()),
("header", blocks.HomepageHeaderBlock()),
("hero_banner", blocks.HeroBannerBlock()),
],
verbose_name="Blok pod headerem",
blank=True,
use_json_field=True,
)
content = StreamField(
[
(
"text",
blocks.RichTextBlock(
label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
),
),
("headline", HeadlineBlock()),
],
verbose_name="Obsah stránky",
blank=True,
use_json_field=True,
)
articles_title = models.CharField("Nadpis článků", max_length=256)
election_countdown_datetime = models.DateTimeField(
"Datum a čas pro odpočet do voleb",
null=True,
blank=True,
help_text="Pro skrytí nechte nevyplněné",
)
show_calendar_on_hp = models.BooleanField(
"Zobrazit kalendář dole na homepage", default=True
)
region_map_button_text = models.CharField(
"Text tlačítka mapy", max_length=256, default="Piráti v krajích"
)
calendar_button_text = models.CharField(
"Text tlačítka kalendáře", max_length=256, default="Kalendář"
)
custom_logo = models.ForeignKey(
"wagtailimages.Image", blank=True, null=True, on_delete=models.SET_NULL
)
show_pirati_cz_link = models.BooleanField(
"Zobrazit v záhlaví odkaz 'pirati.cz'", default=True
)
show_eshop_link = models.BooleanField(
"Zobrazit v záhlaví odkaz na pirátský eshop", default=True
)
show_magazine_link = models.BooleanField(
"Zobrazit v záhlaví odkaz na pirátské listy", default=True
)
facebook = models.URLField(
"Facebook URL",
blank=True,
null=True,
default="https://www.facebook.com/ceska.piratska.strana",
)
twitter = models.URLField(
"Twitter URL",
blank=True,
null=True,
default="https://www.twitter.com/PiratskaStrana",
)
youtube = models.URLField(
"YouTube URL",
blank=True,
null=True,
default="https://www.youtube.com/channel/UC_zxYLGrkmrYazYt0MzyVlA",
)
instagram = models.URLField(
"Instagram URL",
blank=True,
null=True,
default="https://www.instagram.com/pirati.cz/",
)
flickr = models.URLField(
"Flickr URL",
blank=True,
null=True,
default="https://www.flickr.com/photos/pirati/",
)
forum = models.URLField(
"Fórum URL", blank=True, null=True, default="https://forum.pirati.cz/"
)
contact_email = models.EmailField("kontaktni email", max_length=250, blank=True)
contact_phone = models.TextField("kontaktni telefon", max_length=250, blank=True)
contact_newcomers = models.URLField(
"URL pro zájemce o členství",
blank=True,
null=True,
default="https://nalodeni.pirati.cz",
)
donation_page = models.URLField(
"URL pro příjem darů (tlačítko Darovat)",
blank=True,
null=True,
default="https://dary.pirati.cz",
)
newsletter_list_id = models.CharField(
"ID newsletteru",
max_length=20,
blank=True,
null=True,
help_text="ID newsletteru z Mailtrainu. Po vyplnění se formulář pro odběr newsletteru zobrazí na úvodní stránce a na stránce s kontakty.",
)
newsletter_description = models.CharField(
"Popis newsletteru",
max_length=250,
default="Fake news tam nenajdeš, ale dozvíš se, co chystáme doopravdy!",
)
# Lide uvedeni v paticce
footer_person_list = StreamField(
[
("footer_person_list", blocks.PersonCustomPositionBlock()),
],
verbose_name="Osoby v zápatí webu",
blank=True,
max_num=6,
use_json_field=True,
)
# Extra komentar v paticce
footer_extra_content = RichTextField(
verbose_name="Extra obsah pod šedou patičkou",
blank=True,
features=RICH_TEXT_DEFAULT_FEATURES,
)
# settings
matomo_id = models.IntegerField(
"Matomo ID pro sledování návštěvnosti", blank=True, null=True
)
fallback_image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
null=True,
related_name="+",
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("subheader"),
FieldPanel("content"),
FieldPanel("articles_title"),
FieldPanel("election_countdown_datetime"),
FieldPanel("show_calendar_on_hp"),
]
promote_panels = make_promote_panels(admin_help.build(admin_help.IMPORTANT_TITLE))
settings_panels = [
FieldPanel("custom_logo"),
FieldPanel("matomo_id"),
FieldPanel("title_suffix"),
MultiFieldPanel(
[
FieldPanel("show_pirati_cz_link"),
FieldPanel("show_eshop_link"),
FieldPanel("show_magazine_link"),
FieldPanel("donation_page"),
FieldPanel("contact_newcomers"),
FieldPanel("facebook"),
FieldPanel("twitter"),
FieldPanel("youtube"),
FieldPanel("instagram"),
FieldPanel("flickr"),
FieldPanel("forum"),
],
gettext_lazy("Odkazy na webu"),
),
MultiFieldPanel(
[
FieldPanel("contact_email"),
FieldPanel("contact_phone"),
],
gettext_lazy("Kontakty"),
),
MultiFieldPanel(
[
FieldPanel("footer_person_list"),
],
gettext_lazy("Lidé v zápatí stránky"),
),
MultiFieldPanel(
[
FieldPanel("footer_extra_content"),
],
gettext_lazy("Extra obsah v patičce"),
),
MultiFieldPanel(
[
FieldPanel("region_map_button_text"),
FieldPanel("calendar_button_text"),
MultiFieldPanel(
[
FieldPanel("calendar_url"),
PageChooserPanel("calendar_page"),
],
"Kalendář",
),
],
gettext_lazy("Nastavení lišty s kalendářem a mapou"),
),
MultiFieldPanel(
[
FieldPanel("newsletter_list_id"),
FieldPanel("newsletter_description"),
],
gettext_lazy("Formulář pro odběr newsletteru"),
),
FieldPanel("footer_links"),
FieldPanel("fallback_image"),
]
### EDIT HANDLERS
edit_handler = TabbedInterface(
[
ObjectList(content_panels, heading="Obsah"),
ObjectList(promote_panels, heading="Propagovat"),
ObjectList(settings_panels, heading="Nastavení"),
ObjectList(MenuMixin.menu_panels, heading="Menu"),
]
)
### RELATIONS
subpage_types = [
"district.DistrictArticlesPage",
"district.DistrictCenterPage",
"district.DistrictContactPage",
"district.DistrictCrossroadPage",
"district.DistrictCustomPage",
"district.DistrictElectionRootPage",
"district.DistrictPeoplePage",
"district.DistrictProgramPage",
"district.DistrictInteractiveProgramPage",
"district.DistrictGeoFeatureCollectionPage",
"district.DistrictCalendarPage",
"district.DistrictPdfPage",
]
### OTHERS
class Meta:
verbose_name = "Oblastní sdružení"
@route(r"^sdilene/$", name="shared")
def shared(self, request):
return self.setup_article_page_context(request)
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
@property
def articles(self):
return self.materialize_shared_articles_query(
self.append_all_shared_articles_query(
DistrictArticlePage.objects.descendant_of(self)
)[:6]
)
@property
def articles_page(self):
return self._first_subpage_of_type(DistrictArticlesPage)
@property
def center_page(self):
return self._first_subpage_of_type(DistrictCenterPage)
@property
def contact_page(self):
return self._first_subpage_of_type(DistrictContactPage)
@property
def election_page(self):
return self._first_subpage_of_type(DistrictElectionRootPage)
@staticmethod
def get_404_response(request):
return render(request, "district/404.html", status=404)
@property
def people_page(self):
return self._first_subpage_of_type(DistrictPeoplePage)
@property
def program_page(self):
return self._first_subpage_of_type(DistrictProgramPage)
@property
def interactive_program_page(self):
return self._first_subpage_of_type(DistrictInteractiveProgramPage)
@property
def root_page(self):
return self
@property
def has_calendar(self):
return self.calendar_id is not None
@property
def newsletter_info(self):
if not self.newsletter_list_id:
return None
return {
"list_id": self.newsletter_list_id,
"description": self.newsletter_description,
}
class DistrictArticleTag(TaggedItemBase):
content_object = ParentalKey(
"district.DistrictArticlePage",
on_delete=models.CASCADE,
related_name="tagged_items",
)
class DistrictArticlePage(
ArticleMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
author_page = models.ForeignKey(
"district.DistrictPersonPage", on_delete=models.SET_NULL, null=True, blank=True
)
is_black = models.BooleanField("Má tmavé pozadí?", default=False)
tags = ClusterTaggableManager(through=DistrictArticleTag, blank=True)
shared_tags = ClusterTaggableManager(
verbose_name="Tagy pro sdílení mezi weby",
through=SharedTaggedDistrictArticle,
blank=True,
)
thumb_image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="náhledový obrázek",
related_name="thumb_image",
)
search_fields = ArticleMixin.search_fields + [
index.SearchField("author_page"),
index.FilterField("slug"),
]
### PANELS
content_panels = ArticleMixin.content_panels + [
FieldPanel("author_page"),
FieldPanel("is_black"),
FieldPanel("tags"),
FieldPanel("shared_tags"),
FieldPanel("thumb_image"),
]
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 = ["district.DistrictArticlesPage"]
subpage_types = ["district.DistrictPdfPage"]
### OTHERS
class Meta:
verbose_name = "Aktualita"
def clean(self):
cleaned_data = super().clean()
if not self.image and not self.thumb_image:
raise ValidationError("Musí být nahraný buď obrázek nebo náhledový obrázek")
return cleaned_data
def get_context(self, request):
context = super().get_context(request)
context["related_articles"] = (
(
self.get_siblings(inclusive=False)
.live() # TODO? filtrovat na stejné tagy? nebo sdílené články?
.specific()
.order_by("-districtarticlepage__timestamp")[:3]
)
if self.shared_from is None
else []
)
return context
class DistrictArticlesPage(
RoutablePageMixin,
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
ArticlesPageMixin,
Page,
):
### FIELDS
last_import_log = models.TextField(
"Výstup z posledního importu", null=True, blank=True
)
max_items = models.IntegerField("Počet článků na stránce", default=12)
### PANELS
content_panels = ArticlesPageMixin.content_panels + [
FieldPanel("max_items"),
]
promote_panels = make_promote_panels()
import_panels = [
MultiFieldPanel(
[
FieldPanel("do_import"),
FieldPanel("collection"),
FieldPanel("dry_run"),
FieldPanel("use_git"),
FieldPanel("jekyll_repo_url"),
FieldPanel("readonly_log"),
HelpPanel(
mark_safe(
"Import provádějte vždy až po vytvoření stránky aktualit. "
"Pro uložení logu je nutné volit možnost <strong>Publikovat</strong>, nikoliv "
"pouze <strong>Uložit koncept</strong>. "
"Import proběhne na pozadí a může trvat až několik minut. "
"Dejte si po spuštění importu kávu a potom obnovte stránku pro "
"zobrazení výsledku importu."
)
),
],
"import z Jekyll repozitáře",
),
]
### EDIT HANDLERS
edit_handler = TabbedInterface(
[
ObjectList(content_panels, heading="Obsah"),
ObjectList(promote_panels, heading="Propagovat"),
ObjectList(import_panels, heading="Import"),
]
)
### RELATIONS
parent_page_types = ["district.DistrictHomePage"]
subpage_types = ["district.DistrictArticlePage"]
### OTHERS
base_form_class = JekyllImportForm
class Meta:
verbose_name = "Aktuality"
def get_context(self, request):
context = super().get_context(request)
context["articles"] = self.get_page_with_shared_articles(
self.append_all_shared_articles_query(
DistrictArticlePage.objects.child_of(self)
),
self.max_items,
request.GET.get("page", 1),
)
return context
@route(r"^tagy/$", name="tags")
def tags(self, request):
return render(
request,
"district/district_tags_page.html",
context=self.get_tags_page_context(request=request),
)
@route(r"^sdilene/$", name="shared")
def shared(self, request):
return self.setup_article_page_context(request)
def get_tags_page_context(self, request) -> dict:
# Potřebujeme IDčka článků pro správnou root_page pro filtrování zobrazených
# tagů i samotných stránek, protože se filtrují přes specifický field
# (tags__slug)
context = super().get_context(request)
articles = self.materialize_articles_as_id_only(
self.append_all_shared_articles_query(
DistrictArticlePage.objects.child_of(self)
)
)
page_ids = list(map(lambda article: article.page_ptr.id, articles))
# Naplním "tag" a "article_page_list" parametry
context.update(**self.get_tag_and_articles(request, page_ids))
context["tag_list"] = self.get_tag_qs(articles)
# Pro obecnou paginaci posílám "extra_query", abych si podržel tag pro další GET
context["extra_query"] = "&tag={}".format(request.GET.get("tag", ""))
return context
def get_tag_and_articles(self, request, page_ids: list) -> dict:
"""
Vrátí vyfiltrované články podle tagu a page query pro z daného "výběru"
pro danou stránku (site_article_ids). Lepší by bylo články a tag řešit
separátně, ale pak by se musel rozpadnout ten try/except na více bloků.
"""
article_page_qs = None
tag = None
try:
tag = Tag.objects.filter(slug=request.GET["tag"])[0]
article_page_qs = self.append_all_shared_articles_query(
DistrictArticlePage.objects.filter(
page_ptr_id__in=page_ids, tags__slug=tag.slug
),
custom_article_query=lambda shared: shared.filter(
page_ptr_id__in=page_ids, tags__slug=tag.slug
),
)
except (KeyError, IndexError):
tag = None
article_page_qs = self.append_all_shared_articles_query(
DistrictArticlePage.objects.filter(page_ptr_id__in=page_ids),
custom_article_query=lambda shared: shared.filter(
page_ptr_id__in=page_ids
),
)
return {
"article_page_list": self.get_page_with_shared_articles(
article_page_qs, self.max_items, request.GET.get("page", 1)
),
"tag": tag,
}
def get_tag_qs(self, articles: list) -> models.QuerySet:
"""
Getuje Tagy pouze pro DistrictArticlePage omezeno IDčky getnutých přes
root_page. Počítá, kolik článků je s daným tagem.
"""
return self.search_tags_with_count(articles)
class DistrictContactPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
contact_people = StreamField(
[("item", blocks.PersonCustomPositionBlock())],
verbose_name="Kontakty",
blank=True,
use_json_field=True,
)
text = RichTextField("Text", blank=True, features=RICH_TEXT_DEFAULT_FEATURES)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("contact_people"),
FieldPanel("text"),
]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["district.DistrictHomePage"]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Kontakty"
class DistrictPersonTag(TaggedItemBase):
content_object = ParentalKey(
"district.DistrictPersonPage",
on_delete=models.CASCADE,
related_name="tagged_items",
)
class DistrictPersonPage(
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
CalendarMixin,
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", blocks.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",
),
FieldPanel("calendar_url"),
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 = ["district.DistrictPeoplePage"]
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 DistrictPeoplePage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
content = StreamField(
[
(
"text",
blocks.RichTextBlock(
label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
),
),
("people_group", blocks.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 = ["district.DistrictHomePage"]
subpage_types = ["district.DistrictPersonPage"]
### OTHERS
class Meta:
verbose_name = "Lidé"
class DistrictElectionBasePage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
content = StreamField(
CONTENT_BLOCKS
+ [
("badge", blocks.PersonBadgeBlock()),
],
verbose_name="Obsah",
blank=True,
use_json_field=True,
)
campaign_funding_info = models.URLField(
"URL pro zjištění informací o financování kampaně",
blank=True,
null=True,
help_text="Pokud ponecháte prázdné, použije se buď odkaz z nadřazené stránky, nebo https://wiki.pirati.cz/ft/start.",
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("content"),
FieldPanel("campaign_funding_info"),
]
promote_panels = make_promote_panels()
settings_panels = []
class Meta:
abstract = True
@cached_property
def root_election_page(self):
if isinstance(self, DistrictElectionRootPage):
return self
return (
self.get_ancestors()
.type(DistrictElectionRootPage)
.live()
.specific()
.first()
)
class DistrictElectionSubCampaignPageMixin:
@cached_property
def campaign_page(self):
return (
self.get_ancestors()
.type(DistrictElectionCampaignPage)
.live()
.specific()
.first()
)
class DistrictPostElectionStrategyPage(
DistrictElectionSubCampaignPageMixin, DistrictElectionBasePage
):
### FIELDS
perex = models.TextField("Perex", help_text="Pro přehled volebního programu")
content_panels = DistrictElectionBasePage.content_panels + [
FieldPanel("perex"),
]
### RELATIONS
parent_page_types = ["district.DistrictElectionCampaignPage"]
subpage_types = []
class Meta:
verbose_name = "Povolební strategie"
def get_meta_description(self):
if self.search_description:
return self.search_description
return self.perex
class DistrictElectionProgramPage(
DistrictElectionSubCampaignPageMixin, DistrictElectionBasePage
):
### FIELDS
guarantor = models.ForeignKey(
"district.DistrictPersonPage",
verbose_name="Garant",
on_delete=models.PROTECT,
blank=True,
null=True,
)
image = models.ForeignKey(
"wagtailimages.Image",
verbose_name="Ilustrační obrázek",
on_delete=models.PROTECT,
related_name="+",
)
perex = models.TextField("Perex", help_text="Pro přehled volebního programu")
### PANELS
content_panels = DistrictElectionBasePage.content_panels + [
PageChooserPanel("guarantor"),
FieldPanel("image"),
FieldPanel("perex"),
]
### RELATIONS
parent_page_types = ["district.DistrictElectionCampaignPage"]
subpage_types = []
class Meta:
verbose_name = "Bod programu voleb"
def get_meta_image(self):
return self.search_image or self.image or self.root_page.get_meta_image()
def get_meta_description(self):
if self.search_description:
return self.search_description
return self.perex
class DistrictElectionCampaignPage(DistrictElectionBasePage):
### FIELDS
number = models.CharField(
"Zvolené číslo kandidátní listiny", blank=True, null=True, max_length=4
)
candidates = StreamField(
[
("candidates", blocks.CandidateListBlock()),
],
verbose_name="Kandidátní listina",
blank=True,
use_json_field=True,
)
candidate_list_title = models.CharField(
"Titulek kandidátní listiny",
blank=True,
null=True,
max_length=128,
help_text="Např. Kandidátní listina pro magistrát.",
)
program_point_list_title = models.CharField(
"Titulek programové sekce",
blank=True,
null=True,
max_length=128,
help_text="Např. Program pro magistrát.",
)
show_program_points_inline = models.BooleanField(
"Zobrazit obsah programu na jedné stránce",
default=False,
help_text="Hodí se v případě spousty krátkých bodů programu, z nichž si většina nezaslouží vlastní stránku.",
)
hero_headline = models.CharField(
"Banner headline",
max_length=128,
blank=True,
null=True,
help_text="Použije se v hlavním banneru. Pokud je toto hlavní kampaň voleb, nebo ve vaší obci je jen jedna jediná kandidátní listina, můžete ponechat prázdné a pro titulek se pak použije titulek root volební stránky.",
)
hero_motto = models.CharField(
"Motto/claim pro banner",
max_length=128,
blank=True,
null=True,
help_text="Použije se v hlavním banneru.",
)
hero_cta_buttons = StreamField(
[
("button_group", ButtonGroupBlock()),
],
verbose_name="CTAs pro banner",
blank=True,
null=True,
help_text="Použije se v hlavním banneru.",
use_json_field=True,
)
hero_image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
related_name="+",
verbose_name="Ilustrační obrázek pro banner",
help_text="Pokud ponecháte prázdné, použije se výchozí obrázek stránek.",
null=True,
blank=True,
)
hero_candidates_image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
related_name="+",
verbose_name="Obrázek s kandidáty pro banner",
help_text="Použije se jako overlay v hlavním banneru a proto by fotka měla mít průhlednost aby to nevypadalo divně.",
null=True,
blank=True,
)
order = models.SmallIntegerField(
"Pořadí",
default=0,
help_text="Čím nižší pořadí, tím důležitější kampaň je. Bude použito při řazených výpisech a nejdůležitější kampaň bude automaticky routovaná pokud někdo přistoupí na volební rozcestník.",
)
### PANELS
content_panels = Page.content_panels + [
HelpPanel(
"<strong>TIP: </strong>Body programu a stránku povolební strategie přidávejte jako podstránky této stránky"
),
FieldPanel("candidates"),
MultiFieldPanel(
[
FieldPanel("number"),
FieldPanel("candidate_list_title"),
FieldPanel("program_point_list_title"),
FieldPanel("show_program_points_inline"),
FieldPanel("content"),
],
"Personalizace",
),
MultiFieldPanel(
[
FieldPanel("hero_headline"),
FieldPanel("hero_motto"),
FieldPanel("hero_image"),
FieldPanel("hero_candidates_image"),
FieldPanel("hero_cta_buttons"),
],
"Hero banner",
),
FieldPanel("order"),
FieldPanel("campaign_funding_info"),
]
### RELATIONS
parent_page_types = ["district.DistrictElectionRootPage"]
subpage_types = [
"district.DistrictElectionProgramPage",
"district.DistrictPostElectionStrategyPage",
]
class Meta:
verbose_name = "Kandidatura"
@cached_property
def post_election_strategy(self):
return (
self.get_children()
.type(DistrictPostElectionStrategyPage)
.live()
.specific()
.first()
)
@cached_property
def program_points(self):
return self.get_children().type(DistrictElectionProgramPage).live().specific()
def get_meta_image(self):
return (
self.search_image
or self.hero_candidates_image
or self.hero_image
or self.root_page.get_meta_image()
)
def get_meta_description(self):
if self.search_description:
return self.search_description
return self.hero_motto
class DistrictElectionRootPage(RoutablePageMixin, Page):
"""The election root page only serves as a wrapper for sharing stuff among campaign pages.
It is never rendered on its own. When accessed, it will automatically redirect to the first campaign page.
"""
### PANELS
### RELATIONS
parent_page_types = [
"district.DistrictHomePage",
"district.DistrictCustomPage",
"district.DistrictCrossroadPage",
]
subpage_types = [
"district.DistrictElectionCampaignPage",
"district.DistrictGeoFeatureCollectionPage",
]
### OTHERS
class Meta:
verbose_name = "Volební rozcestník"
@cached_property
def campaigns(self):
return (
self.get_children()
.type(DistrictElectionCampaignPage)
.live()
.order_by("districtelectioncampaignpage__order")
)
@cached_property
def primary_campaign(self):
return self.campaigns.first()
@route(r"^$")
def index(self, request):
"""When accessed, redirect to first campaign page available or trigger 404."""
if not self.primary_campaign:
return HttpResponseNotFound()
return HttpResponseRedirect(self.primary_campaign.get_url())
class DistrictInteractiveProgramPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
perex = models.TextField("Perex", blank=True)
content = StreamField(
[("interactive_program_block", blocks.InteractiveProgramBlock())],
verbose_name="Části programu",
blank=False,
use_json_field=True,
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("perex"),
FieldPanel("content"),
]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["district.DistrictHomePage"]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Interaktivní program"
def save(self, **kwargs):
from redmine_utils.functions import fill_data_from_redmine_for_page
fill_data_from_redmine_for_page(self)
return super().save(**kwargs)
def get_meta_description(self):
if self.search_description:
return self.search_description
return self.perex
class DistrictProgramPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
perex = models.TextField("Perex", blank=True)
content = StreamField(
[
("static_program_block", blocks.StaticProgramBlock()),
("redmine_program_block", blocks.RedmineProgramBlock()),
],
verbose_name="obsah stránky",
blank=True,
use_json_field=True,
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("perex"),
FieldPanel("content"),
]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["district.DistrictHomePage"]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Plnění programu"
def save(self, **kwargs):
from redmine_utils.functions import fill_data_from_redmine_for_page
fill_data_from_redmine_for_page(self)
return super().save(**kwargs)
def get_meta_description(self):
if self.search_description:
return self.search_description
return self.perex
class DistrictCenterPage(
CalendarMixin, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
calendar_page = models.ForeignKey(
"DistrictCalendarPage",
verbose_name="Stránka s kalendářem",
on_delete=models.PROTECT,
null=True,
blank=True,
)
perex = models.TextField("Perex", blank=True, null=True)
background_photo = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
related_name="+",
)
content = StreamField(
CONTENT_BLOCKS
+ [
("badge", blocks.PersonBadgeBlock()),
],
verbose_name="Obsah",
blank=True,
use_json_field=True,
)
text = RichTextField("Text", blank=True, null=True)
sidebar_content = StreamField(
[
("map", MapPointBlock()),
("figure", FigureBlock()),
("youtube", YouTubeVideoBlock(label="YouTube video")),
("address", blocks.AddressBlock()),
("contact", blocks.CenterContactBlock()),
("badge", blocks.PersonBadgeBlock()),
],
verbose_name="Obsah bočního panelu",
blank=True,
use_json_field=True,
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("perex"),
FieldPanel("background_photo"),
FieldPanel("text"),
FieldPanel("content"),
MultiFieldPanel(
[
FieldPanel("calendar_url"),
PageChooserPanel("calendar_page"),
],
"Kalendář",
),
PageChooserPanel("calendar_page"),
FieldPanel("sidebar_content"),
]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["district.DistrictHomePage"]
subpage_types = ["district.DistrictCalendarPage"]
### OTHERS
class Meta:
verbose_name = "Stránka pirátského centra"
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
)
@property
def has_calendar(self):
return self.calendar_id is not None
def get_meta_image(self):
return (
self.search_image
or self.background_photo
or self.root_page.get_meta_image()
)
def get_meta_description(self):
if self.search_description:
return self.search_description
desc = None
if self.perex:
desc = self.perex
elif self.text:
desc = trim_to_length(strip_all_html_tags(self.text))
return desc
class DistrictCrossroadPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
cards_content = StreamField(
[("cards", blocks.CardLinkWithHeadlineBlock())],
verbose_name="Karty rozcestníku",
blank=True,
use_json_field=True,
)
content = StreamField(
CONTENT_BLOCKS
+ [
("badge", blocks.PersonBadgeBlock()),
("people_group", blocks.PeopleGroupListBlock()),
],
verbose_name="Obsah stránky",
blank=True,
use_json_field=True,
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("cards_content"),
FieldPanel("content"),
]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["district.DistrictHomePage"]
subpage_types = [
"district.DistrictArticlePage",
"district.DistrictArticlesPage",
"district.DistrictCenterPage",
"district.DistrictContactPage",
"district.DistrictCrossroadPage",
"district.DistrictCustomPage",
"district.DistrictElectionRootPage",
"district.DistrictGeoFeatureCollectionPage",
"district.DistrictPeoplePage",
"district.DistrictPersonPage",
"district.DistrictProgramPage",
"district.DistrictInteractiveProgramPage",
]
### OTHERS
class Meta:
verbose_name = "Rozcestník s kartami"
class DistrictCustomPage(
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
RoutablePageMixin,
Page,
):
### FIELDS
content = StreamField(
CONTENT_BLOCKS
+ [
("badge", blocks.PersonBadgeBlock()),
("people_group", blocks.PeopleGroupListBlock()),
("newsletter", NewsletterSubscriptionBlock()),
],
verbose_name="Obsah",
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 = ["district.DistrictHomePage", "district.DistrictCrossroadPage"]
subpage_types = ["district.DistrictElectionRootPage"]
### OTHERS
class Meta:
verbose_name = "Libovolná vlastní stránka"
class DistrictGeoFeatureCollectionPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, Page
):
### FIELDS
perex = models.TextField("Perex", null=True)
hero_cta_buttons = StreamField(
[
("button_group", ButtonGroupBlock()),
],
verbose_name="CTAs pro banner",
blank=True,
null=True,
help_text="Použije se v hlavním banneru.",
use_json_field=True,
)
content = StreamField(
CONTENT_BLOCKS
+ [
("badge", blocks.PersonBadgeBlock()),
("people_group", blocks.PeopleGroupListBlock()),
],
verbose_name="Obsah úvodní",
blank=True,
use_json_field=True,
)
content_after = StreamField(
CONTENT_BLOCKS
+ [
("badge", blocks.PersonBadgeBlock()),
("people_group", blocks.PeopleGroupListBlock()),
],
verbose_name="Obsah za mapou",
blank=True,
use_json_field=True,
)
content_footer = StreamField(
CONTENT_BLOCKS
+ [
("badge", blocks.PersonBadgeBlock()),
("people_group", blocks.PeopleGroupListBlock()),
],
verbose_name="Obsah v patičkové části",
blank=True,
use_json_field=True,
)
image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="Obrázek na pozadí",
related_name="+",
)
logo_image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="Logo",
related_name="+",
)
style = models.CharField(
"Styl mapy", choices=MAP_STYLES, max_length=50, default=DEFAULT_MAP_STYLE
)
map_title = models.TextField("Titulek mapy", blank=True, null=True)
category_list_title = models.TextField(
"Titulek přehledu dle kategorie", blank=True, null=True
)
promoted_block_title = models.TextField(
"Titulek bloku propagovaných položek", blank=True, null=True
)
### PANELS
content_panels = Page.content_panels + [
MultiFieldPanel(
[
FieldPanel("perex"),
FieldPanel("hero_cta_buttons"),
FieldPanel("content"),
FieldPanel("content_after"),
FieldPanel("content_footer"),
FieldPanel("promoted_block_title"),
FieldPanel("logo_image"),
FieldPanel("image"),
],
"Obsah hlavní stránky kolekce",
),
MultiFieldPanel(
[
InlinePanel("categories"),
FieldPanel("category_list_title"),
],
"Kategorie",
),
MultiFieldPanel(
[
FieldPanel("map_title"),
FieldPanel("style"),
],
"Nastavení mapy",
),
]
settings_panels = []
### RELATIONS
parent_page_types = [
"district.DistrictHomePage",
"district.DistrictElectionRootPage",
]
subpage_types = ["district.DistrictGeoFeatureDetailPage"]
class Meta:
verbose_name = "Stránka s mapovou kolekcí"
def get_features_by_category(self):
features = (
self.get_children()
.live()
.specific()
.prefetch_related("category")
.order_by("districtgeofeaturedetailpage__sort_order")
)
categories = sorted(set(f.category for f in features), key=lambda c: c.name)
return [
(category, [f for f in features if f.category == category])
for category in categories
]
def get_promoted_features(self):
return (
self.get_children()
.live()
.specific()
.filter(districtgeofeaturedetailpage__promoted=True)
.order_by("districtgeofeaturedetailpage__sort_order")
)
def get_context(self, request):
context = super().get_context(request)
features_by_category = self.get_features_by_category()
context["features_by_category"] = features_by_category
context["promoted_features"] = self.get_promoted_features()
context["js_map"] = {
"tile_server_config": json.dumps(TILE_SERVER_CONFIG),
"style": self.style,
"categories": json.dumps(
[
# Gather all categories used in collection
{"name": c.name, "color": c.hex_color}
for c, f in features_by_category
]
),
"geojson": json.dumps(
[
f.as_geojson_object(request)
for c, features in features_by_category
for f in features
]
),
}
return context
def get_meta_image(self):
return (
self.search_image
or self.logo_image
or self.image
or self.root_page.get_meta_image()
)
def get_meta_description(self):
if self.search_description:
return self.search_description
return self.perex
class DistrictGeoFeatureCollectionCategory(Orderable):
name = models.CharField("Název", max_length=100)
hex_color = models.CharField(
"Barva (HEX)",
max_length=6,
help_text="Zadejte barvu pomocí HEX notace (bez # na začátku).",
)
page = ParentalKey(
DistrictGeoFeatureCollectionPage,
on_delete=models.CASCADE,
related_name="categories",
)
### PANELS
panels = [
FieldPanel("name"),
FieldPanel("hex_color"),
]
class Meta:
verbose_name = "Kategorie mapové kolekce"
def __str__(self) -> str:
return f"{self.page} / {self.name}"
@property
def rgb(self):
return tuple(int(self.hex_color[i : i + 2], 16) for i in (0, 2, 4))
def make_feature_index_cache_key(feature: "DistrictGeoFeatureDetailPage"):
return f"DistrictGeoFeatureDetailPage::{feature.id}::index"
class DistrictCalendarPage(SubpageMixin, MetadataPageMixin, CalendarMixin, Page):
"""
Page for displaying full calendar
"""
### PANELS
content_panels = Page.content_panels + [FieldPanel("calendar_url")]
### RELATIONS
parent_page_types = [
"district.DistrictCenterPage",
"district.DistrictHomePage",
]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Stránka s kalendářem"
class DistrictGeoFeatureDetailPage(
ExtendedMetadataPageMixin, MetadataPageMixin, SubpageMixin, Page, Orderable
):
perex = models.TextField("Perex", null=False)
geojson = models.TextField(
"Geodata",
help_text="Vložte surový GeoJSON objekt typu 'FeatureCollection'. Vyrobit jej můžete např. pomocí online služby geojson.io. Pokud u Feature objektů v kolekci poskytnete properties 'title' a 'description', zobrazí se jak na mapě, tak i v detailu.",
null=False,
)
category = models.ForeignKey(
"district.DistrictGeoFeatureCollectionCategory",
verbose_name="Kategorie",
blank=False,
null=False,
on_delete=models.PROTECT,
)
guarantor = models.ForeignKey(
"district.DistrictPersonPage",
verbose_name="Garant",
on_delete=models.PROTECT,
null=True,
blank=True,
)
image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="obrázek",
)
content = StreamField(
CONTENT_BLOCKS
+ [
("badge", blocks.PersonBadgeBlock()),
("people_group", blocks.PeopleGroupListBlock()),
],
verbose_name="Obsah",
blank=True,
use_json_field=True,
)
parts_section_title = models.CharField(
"Titulek přehledu součástí",
help_text="Pokud ponecháte prázdné, nebude se titulek zobrazovat. Sekce se vůbec nezobrazí pokud GeoJSON FeatureCollection obsahuje jen jeden Feature.",
max_length=100,
null=True,
blank=True,
)
initial_zoom = models.IntegerField(
"Výchozí zoom",
default=15,
null=False,
blank=False,
)
promoted = models.BooleanField(
"Propagovat",
default=False,
)
sort_order = models.IntegerField(
"Index řazení",
null=True,
blank=True,
help_text="Čím větší hodnotu zadáte, tím později ve výpise položek bude. Můžete tím tedy ovlivňovat očíslování "
"položky ve výpisu.",
default=0,
)
sort_order_field = "sort_order"
### PANELS
content_panels = [
MultiFieldPanel(
[
FieldPanel("title"),
FieldPanel("perex"),
FieldPanel("content"),
FieldPanel("parts_section_title"),
FieldPanel("image"),
FieldPanel("category"),
FieldPanel("promoted"),
],
"Základní informace",
),
MultiFieldPanel(
[
FieldPanel("geojson"),
FieldPanel("initial_zoom"),
],
"Mapka",
),
PageChooserPanel("guarantor"),
FieldPanel("sort_order"),
]
settings_panels = []
### RELATIONS
parent_page_types = ["district.DistrictGeoFeatureCollectionPage"]
subpage_types = []
class Meta:
verbose_name = "Položka mapové kolekce"
ordering = ["sort_order"]
def save(self, *args, **kwargs):
# delete all sibling index cache keys to force recompute
keys = [
make_feature_index_cache_key(feature)
for feature in self.get_siblings(inclusive=True).live()
]
cache.delete_many(keys)
return super().save(*args, **kwargs)
@property
def index(self):
key = make_feature_index_cache_key(self)
cached_index = cache.get(key)
if cached_index is None:
cached_index = (
list(
self.get_siblings(inclusive=True)
.live()
.order_by("districtgeofeaturedetailpage__sort_order")
.values_list("pk", flat=True)
).index(self.pk)
+ 1
)
cache.set(key, cached_index)
return cached_index
def get_meta_image(self):
return self.search_image or self.image
def get_meta_description(self):
if self.search_description:
return self.search_description
return self.perex
def as_geojson_object(self, request):
collection = json.loads(self.geojson)
image = (
self.image.get_rendition("fill-1200x675-c75|jpegquality-80").url
if self.image
else None
)
url = self.get_url(request) if self.content else None
for idx, feature in enumerate(collection["features"]):
feature["properties"].update(
{
"id": self.pk,
"index": self.index,
"slug": f"{idx}-{self.pk}-{self.slug}",
"collectionTitle": self.title,
"collectionDescription": self.perex,
"image": image,
"link": url,
"category": self.category.name,
}
)
if not "description" in feature["properties"]:
feature["properties"]["description"] = None
# This extends GeoJSON spec to pass down more context to the map.
collection["properties"] = {}
collection["properties"]["slug"] = f"{self.pk}-{self.slug}"
collection["properties"]["collectionTitle"] = self.title
collection["properties"]["collectionDescription"] = self.perex
collection["properties"]["category"] = self.category.name
collection["properties"]["index"] = self.index
collection["properties"]["image"] = image
collection["properties"]["link"] = url
return collection
def get_context(self, request):
context = super().get_context(request)
context[
"features_by_category"
] = self.get_parent().specific.get_features_by_category()
feature_collection = self.as_geojson_object(request)
context["js_map"] = {
"tile_server_config": json.dumps(TILE_SERVER_CONFIG),
"style": self.get_parent().specific.style,
"categories": json.dumps(
[{"name": self.category.name, "color": self.category.hex_color}]
),
"geojson": json.dumps([feature_collection]),
}
context["parts"] = [f["properties"] for f in feature_collection["features"]]
return context
def clean(self):
try:
self.geojson = maps_validators.normalize_geojson_feature_collection(
self.geojson, allowed_types=SUPPORTED_FEATURE_TYPES
)
except ValueError as exc:
raise ValidationError({"geojson": str(exc)}) from exc
class DistrictPdfPage(PdfPageMixin, MetadataPageMixin, SubpageMixin, Page):
"""
Single pdf page display
"""
### RELATIONS
parent_page_types = [
"district.DistrictHomePage",
"district.DistrictArticlePage",
]
subpage_types = []
### PANELS
content_panels = Page.content_panels + PdfPageMixin.content_panels
### OTHER
class Meta:
verbose_name = "PDF stránka"