-
Tomáš Valenta authoredTomáš Valenta authored
models.py 28.14 KiB
from functools import cached_property
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib import messages
from django.core.paginator import Paginator
from django.db import models
from django.forms.models import model_to_dict
from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import render
from django.utils import timezone
from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey
from taggit.models import Tag, TaggedItemBase
from wagtail.admin.panels import (
FieldPanel,
HelpPanel,
MultiFieldPanel,
ObjectList,
PageChooserPanel,
TabbedInterface,
)
from wagtail.blocks import PageChooserBlock, RichTextBlock
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.fields import RichTextField, StreamField
from wagtail.models import Page
from wagtail.search import index
from wagtailmetadata.models import MetadataPageMixin
from calendar_utils.models import CalendarMixin
from shared.forms import SubscribeForm
from shared.models import ( # MenuMixin,
ArticleMixin,
ArticlesMixin,
ArticlesPageMixin,
ExtendedMetadataHomePageMixin,
ExtendedMetadataPageMixin,
SharedTaggedMainArticle,
SubpageMixin,
)
from shared.utils import make_promote_panels, subscribe_to_newsletter
from tuning import admin_help
from . import blocks
from .constants import MONTH_NAMES
from .forms import JekyllImportForm
from .menu import MenuMixin, PageInMenuMixin
class MainHomePage(
MenuMixin,
RoutablePageMixin,
ExtendedMetadataHomePageMixin,
MetadataPageMixin,
ArticlesMixin,
Page,
):
# header
menu_button_name = models.CharField(
verbose_name="Text na tlačítku pro zapojení", max_length=16
)
menu_button_content = StreamField(
[
("navbar_menu_item", blocks.NavbarMenuItemBlock()),
],
verbose_name="Obsah menu pro zapojení se",
blank=True,
use_json_field=True,
)
# content
content = StreamField(
[
("carousel", blocks.HomePageCarouseSlideBlock()),
("news", blocks.NewsBlock()),
("europarl_news", blocks.EuroparlNewsBlock()),
("people", blocks.PeopleOverviewBlock()),
("regions", blocks.RegionsBlock()),
("boxes", blocks.BoxesBlock()),
],
verbose_name="Hlavní obsah",
blank=True,
use_json_field=True,
)
# footer
footer_other_links = StreamField(
[
("other_links", blocks.OtherLinksBlock()),
],
verbose_name="Odkazy v zápatí webu",
blank=True,
use_json_field=True,
)
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,
)
matomo_id = models.IntegerField(
"Matomo ID pro sledování návštěvnosti", blank=True, null=True
)
social_links = StreamField(
[
("social_links", blocks.SocialLinkBlock()),
],
verbose_name="Odkazy na sociální sítě",
blank=True,
use_json_field=True,
)
content_panels = Page.content_panels + [
FieldPanel("content"),
FieldPanel("footer_other_links"),
FieldPanel("footer_person_list"),
]
promote_panels = make_promote_panels(admin_help.build(admin_help.IMPORTANT_TITLE))
settings_panels = [
FieldPanel("menu_button_name"),
FieldPanel("menu_button_content"),
PageChooserPanel("gdpr_and_cookies_page"),
FieldPanel("social_links"),
FieldPanel("matomo_id"),
]
### 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 = [
"main.MainArticlesPage",
"main.MainProgramPage",
"main.MainPeoplePage",
"main.MainPersonPage",
"main.MainSimplePage",
"main.MainContactPage",
"main.MainCrossroadPage",
"main.MainHoaxPage",
"main.MainSearchPage",
]
### OTHERS
class Meta:
verbose_name = "HomePage pirati.cz"
@cached_property
def gdpr_and_cookies_url(self):
if self.gdpr_and_cookies_page:
return self.gdpr_and_cookies_page.url
return "#"
@staticmethod
def get_404_response(request):
return render(request, "main/404.html", status=404)
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, args, kwargs)
context["article_data_list"] = self.materialize_shared_articles_query(
self.append_all_shared_articles_query(MainArticlePage.objects.live().all())
.live()
.order_by("-union_date")[:3]
)
return context
def get_region_response(self, request):
context = {}
if request.GET.get("region", None) == "VSK":
sorted_article_qs = MainArticlePage.objects.filter(
region__isnull=False
).order_by("-date")
context = {"article_data_list": sorted_article_qs[:3]}
else:
sorted_article_qs = MainArticlePage.objects.filter(
region=request.GET.get("region", None)
)[:3]
context = {"article_data_list": sorted_article_qs[:3]}
data = {
"html": render(
request, "main/includes/small_article_preview.html", context
).content.decode("utf-8")
}
return JsonResponse(data=data, safe=False)
def serve(self, request, *args, **kwargs):
if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
if "region" in request.GET:
return self.get_region_response(request)
return super().serve(request, *args, **kwargs)
@cached_property
def newsletter_subscribe_url(self):
newsletter_subscribe = self.reverse_subpage("newsletter_subscribe")
return (
self.url + newsletter_subscribe
if self.url is not None
else newsletter_subscribe
) # preview fix
@property
def articles_page(self):
return self._first_subpage_of_type(MainArticlesPage)
@property
def people_page(self):
return self._first_subpage_of_type(MainPeoplePage)
@property
def contact_page(self):
return self._first_subpage_of_type(MainContactPage)
@property
def search_page(self):
return self._first_subpage_of_type(MainSearchPage)
@property
def root_page(self):
return self
@route(r"^prihlaseni-k-newsletteru/$")
def newsletter_subscribe(self, request):
if request.method == "POST":
form = SubscribeForm(request.POST)
if form.is_valid():
subscribe_to_newsletter(
form.cleaned_data["email"], settings.PIRATICZ_NEWSLETTER_CID
)
messages.success(
request,
"Zkontroluj si prosím schránku, poslali jsme ti potvrzovací email.",
)
try:
page = (
Page.objects.filter(id=form.cleaned_data["return_page_id"])
.live()
.first()
)
return HttpResponseRedirect(page.full_url)
except Page.DoesNotExist:
return HttpResponseRedirect(self.url)
messages.error(
request,
"Tvůj prohlížeč nám odeslal špatná data. Prosím, zkus to znovu.",
)
return HttpResponseRedirect(self.url)
@route(r"^feeds/atom/$")
def view_feed(self, request):
# Avoid circular import
from .feeds import LatestArticlesFeed # noqa
return LatestArticlesFeed()(request, self.articles_page.id)
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 MainArticlesPage(
RoutablePageMixin,
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
ArticlesPageMixin,
PageInMenuMixin,
Page,
):
last_import_log = models.TextField(
"Výstup z posledního importu", null=True, blank=True
)
perex = models.TextField()
import_panels = [
MultiFieldPanel(
[
FieldPanel("do_import"),
FieldPanel("collection"),
FieldPanel("dry_run"),
FieldPanel("jekyll_repo_url"),
FieldPanel("readonly_log"),
HelpPanel(
"Import provádějte vždy až po vytvoření stránky aktualit. "
'Pro uložení logu je nutné volit možnost "Publikovat", nikoliv'
'pouze "Uložit koncept". '
"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",
),
]
### RELATIONS
parent_page_types = ["main.MainHomePage"]
subpage_types = ["main.MainArticlePage"]
### PANELS
content_panels = Page.content_panels + [
FieldPanel("perex"),
FieldPanel("shared_tags"),
]
promote_panels = make_promote_panels()
### EDIT HANDLERS
edit_handler = TabbedInterface(
[
ObjectList(content_panels, heading="Obsah"),
ObjectList(promote_panels, heading="Propagovat"),
ObjectList(import_panels, heading="Import"),
]
)
### OTHERS
base_form_class = JekyllImportForm
class Meta:
verbose_name = "Rozcestník článků"
def get_base_shared_articles_query(self, filter: models.Q):
return self.materialize_shared_articles_query(
self.append_all_shared_articles_query(
MainArticlePage.objects.filter(filter)
)
.live()
.order_by("-union_date")
)
def get_article_data_list(
self,
months_back: int = 1,
search_filter: models.Q | None = None,
):
if search_filter is None:
search_filter = models.Q()
target_date_list = (
self.append_all_shared_articles_query(
MainArticlePage.objects.filter(search_filter)
)
.order_by("-union_date")
.live()
.values_list("union_date", flat=True)
)
if not target_date_list:
return []
target_date = target_date_list[0] - relativedelta(months=months_back)
first_day_of_target_month = target_date.replace(day=1)
filter = models.Q(date__gte=first_day_of_target_month) & search_filter
sorted_article_qs = self.get_base_shared_articles_query(filter)
article_data_list = []
current_month_data = self.get_empty_month_data(timezone.now().date())
for article in sorted_article_qs:
if article.date.month != current_month_data["month_number"]:
if len(current_month_data["articles"]) != 0:
# append completed month if it contains any articles
article_data_list.append(current_month_data)
current_month_data = self.get_empty_month_data(article.date)
current_month_data["articles"].append(article)
article_data_list.append(current_month_data) # last iteration
return article_data_list
def get_search_filters(self, request):
filter = models.Q()
if "tag_id" in request.GET:
tag = self.get_filtered_tag(request)
if tag is not None:
filter = filter & models.Q(tags__id=tag.id)
if "q" in request.GET:
filter = filter & models.Q(title__icontains=self.get_search_query(request))
return filter
def get_filtered_tag(self, request) -> Tag | None:
if "tag_id" in request.GET:
try:
return Tag.objects.filter(id=int(request.GET["tag_id"])).first()
except Exception:
pass
return None
def get_search_query(self, request) -> str | None:
if "q" in request.GET:
return request.GET["q"]
def get_context(self, request, get_articles: bool = True, *args, **kwargs):
ctx = super().get_context(request, args, kwargs)
if get_articles:
filtered_tag = self.get_filtered_tag(request)
if filtered_tag is not None:
ctx["filtered_tag"] = filtered_tag
search_query = self.get_search_query(request)
if search_query is not None:
ctx["search_query"] = search_query
search_filter = self.get_search_filters(request)
article_timeline_list = self.get_article_data_list(1, search_filter)
ctx["article_timeline_list"] = article_timeline_list
ctx["show_next_timeline_articles"] = len(
self.get_base_shared_articles_query(search_filter)
) != 0 and (
self.materialize_shared_articles_query(
self.append_all_shared_articles_query(
MainArticlePage.objects.filter(search_filter)
)
.live()
.order_by("union_date")[:2] # LIMIT 2
)[0]
not in article_timeline_list[-1]["articles"]
)
tags = []
for article in MainArticlePage.objects.all()[:50]:
for tag in article.tags.all():
if tag in tags:
continue
tags.append(tag)
ctx["tags"] = tags
# meow
return ctx
def get_timeline_articles_response(self, request):
try:
months = int(request.GET.get("months", None))
except ValueError:
months = 1
search_filter = self.get_search_filters(request)
article_timeline_list = self.get_article_data_list(months, search_filter)
context = {"article_timeline_list": article_timeline_list}
data = {
"html": render(
request,
"main/includes/organisms/articles/articles_timeline_list.html",
context,
).content.decode("utf-8"),
"has_next": (
len(self.get_base_shared_articles_query(search_filter)) != 0
and (
self.materialize_shared_articles_query(
self.append_all_shared_articles_query(
MainArticlePage.objects.filter(search_filter)
)
.live()
.order_by("union_date")[:2] # LIMIT 2
)[0]
not in article_timeline_list[-1]["articles"]
)
),
}
return JsonResponse(data=data, safe=False)
@route(r"^sdilene/$", name="shared")
def shared(self, request):
return self.setup_article_page_context(request)
def serve(self, request, *args, **kwargs):
if request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
if "months" in request.GET:
return self.get_timeline_articles_response(request)
return super().serve(request, *args, **kwargs)
@staticmethod
def get_empty_month_data(date_obj):
return {
"month_number": date_obj.month,
"month_text": MONTH_NAMES[date_obj.month - 1],
"articles": [],
}
class MainArticleTag(TaggedItemBase):
content_object = ParentalKey(
"main.MainArticlePage",
on_delete=models.CASCADE,
related_name="tagged_items",
)
class MainArticlePage(
ArticleMixin,
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
PageInMenuMixin,
Page,
):
### FIELDS
content = StreamField(
[
(
"text",
RichTextBlock(template="main/includes/atoms/text/prose_richtext.html"),
),
("quote", blocks.ArticleQuoteBlock()),
("download", blocks.ArticleDownloadBlock()),
],
verbose_name="Článek",
blank=True,
use_json_field=True,
)
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="tagged_articles", blank=True
)
shared_tags = ClusterTaggableManager(
verbose_name="Tagy pro sdílení mezi weby",
through=SharedTaggedMainArticle,
blank=True,
)
search_fields = ArticleMixin.search_fields + [
index.SearchField("author_page"),
index.FilterField("slug"),
]
### PANELS
content_panels = ArticleMixin.content_panels + [
FieldPanel("author_page"),
FieldPanel("tags"),
FieldPanel("shared_tags"),
]
promote_panels = make_promote_panels(
admin_help.build(admin_help.NO_SEO_TITLE, admin_help.NO_DESCRIPTION_USE_PEREX),
search_image=False,
)
### RELATIONS
parent_page_types = ["main.MainArticlesPage"]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Aktualita"
# def get_context(self, request): chceme/nechceme?
# context = super().get_context(request)
# context["related_articles"] = (
# self.get_siblings(inclusive=False)
# .live()
# .specific()
# .order_by("-mainarticlepage__date")[:3]
# )
# return context
class MainProgramPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
### FIELDS
program = StreamField(
[
("program_group", blocks.ProgramGroupBlock()),
("program_group_crossroad", blocks.ProgramGroupBlockCrossroad()),
("program_group_popout", blocks.ProgramGroupBlockPopout()),
],
verbose_name="Program",
blank=True,
use_json_field=True,
)
### PANELS
content_panels = Page.content_panels + [FieldPanel("program")]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["main.MainHomePage"]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Program"
class MainPeoplePage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
### FIELDS
perex_col_1 = models.TextField(
verbose_name="Perex - první sloupec",
)
perex_col_2 = models.TextField(
verbose_name="Perex - druhý sloupec",
)
people = StreamField(
[
("people_group", blocks.PeopleGroupBlock(label="Seznam osob")),
("team_group", blocks.TeamBlock()),
],
verbose_name="Lidé a týmy",
blank=True,
use_json_field=True,
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("perex_col_1"),
FieldPanel("perex_col_2"),
FieldPanel("people"),
]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["main.MainHomePage"]
subpage_types = [
"main.MainPersonPage",
"main.MainSimplePage",
]
### OTHERS
@property
def perex(self) -> str:
return self.perex_col_1 + " \n" + self.perex_col_2
class Meta:
verbose_name = "Lidé a týmy"
class MainPersonPage(
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
CalendarMixin,
PageInMenuMixin,
Page,
):
### FIELDS
main_image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="Hlavní obrázek",
related_name="+",
)
profile_image = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="Profilový obrázek",
related_name="+",
)
before_name = models.CharField(
"Tituly před jménem", max_length=32, blank=True, null=True
)
after_name = models.CharField(
"Tituly za jménem", max_length=16, blank=True, null=True
)
position = models.CharField(
"Pozice/povolání", max_length=200, blank=True, null=True
)
primary_group = models.CharField(
"Kategorie",
help_text="např. 'Europarlament' nebo 'Sněmovna'",
max_length=32,
blank=True,
null=True,
)
perex = models.TextField()
text = RichTextField()
social_links = StreamField(
[
("social_links", blocks.SocialLinkBlock()),
],
verbose_name="Odkazy na sociální sítě",
blank=True,
use_json_field=True,
)
related_people = StreamField(
[
(
"person",
PageChooserBlock(page_type="main.MainPersonPage", label="Detail osoby"),
)
],
verbose_name="Další lidé",
blank=True,
use_json_field=True,
)
email = models.CharField("E-mail", max_length=128, blank=True, null=True)
phone = models.CharField("Telefonní kontakt", max_length=16, blank=True, null=True)
settings_panels = []
### RELATIONS
parent_page_types = ["main.MainPeoplePage"]
subpage_types = []
### PANELS
content_panels = Page.content_panels + [
FieldPanel("main_image"),
FieldPanel("profile_image"),
FieldPanel("before_name"),
FieldPanel("after_name"),
FieldPanel("position"),
FieldPanel("perex"),
FieldPanel("text"),
FieldPanel("email"),
FieldPanel("phone"),
FieldPanel("calendar_url"),
FieldPanel("social_links"),
FieldPanel("related_people"),
]
def get_context(self, request) -> dict:
context = super().get_context(request)
context["article_page_list"] = (
MainArticlePage.objects.filter(author_page=self.id)
.order_by("-date")
.live()[:3]
)
return context
### 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
)
class MainSimplePage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
### FIELDS
# content
content = StreamField(
[
(
"text",
RichTextBlock(template="main/includes/atoms/text/prose_richtext.html"),
),
],
verbose_name="Hlavní 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 = [
"main.MainHomePage",
"main.MainSimplePage",
"main.MainCrossroadPage",
"main.MainPeoplePage",
]
subpage_types = ["main.MainSimplePage"]
### OTHERS
class Meta:
verbose_name = "Jednoduchá stárnka"
class MainContactPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
### FIELDS
contact_people = StreamField(
[("item", blocks.PersonContactBlock())],
verbose_name="Kontaktní osoby",
blank=True,
use_json_field=True,
)
contact_boxes = StreamField(
[("item", blocks.PersonContactBoxBlock())],
verbose_name="Kontaktní boxy",
blank=True,
use_json_field=True,
)
text = StreamField(
[("two_columns_text", blocks.TwoTextColumnBlock())],
verbose_name="Kontaktní informace",
blank=True,
use_json_field=True,
)
### PANELS
content_panels = Page.content_panels + [
FieldPanel("text"),
FieldPanel("contact_people"),
FieldPanel("contact_boxes"),
]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["main.MainHomePage"]
subpage_types = []
### OTHERS
class Meta:
verbose_name = "Kontakty"
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 MainSearchPage(
ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin, PageInMenuMixin, Page
):
parent_page_types = ["main.MainHomePage"]
subpage_types = []
class Meta:
verbose_name = "Vyhledávací stránka"
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, args, kwargs)
context["results"] = []
if request.GET.get("q", "") == "":
return context
search_query = request.GET["q"]
context["global_search_query"] = search_query
for model in (
MainPersonPage,
MainArticlePage,
MainSimplePage,
):
filter = models.Q(title__icontains=search_query)
if hasattr(model, "perex"):
filter = filter | models.Q(perex__icontains=search_query)
results = model.objects.filter(filter)
if hasattr(model, "date"):
results = results.order_by("-date")
context["results"] += list(results.all()[:15])
context["results"].sort(key=lambda result: result.title)
return context