-
Tomáš Valenta authoredTomáš Valenta authored
models.py 24.11 KiB
from functools import cached_property
from django.conf import settings
from django.core.cache import cache
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db import models
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from modelcluster.fields import ParentalKey
from wagtail.admin.panels import (
FieldPanel,
InlinePanel,
MultiFieldPanel,
ObjectList,
PublishingPanel,
TabbedInterface,
)
from wagtail.fields import RichTextField, StreamField
from wagtail.images.blocks import ImageChooserBlock
from wagtail.models import Orderable, Page
from wagtailmetadata.models import MetadataPageMixin
from shared.models import (
ExtendedMetadataHomePageMixin,
ExtendedMetadataPageMixin,
SubpageMixin,
)
from shared.utils import get_subpage_url, make_promote_panels
from tuning import admin_help
from .blocks import (
CrowdfundingRewardBlock,
CustomContentBlock,
CustomLinkBlock,
DistrictDonationBlock,
PartySupportFormBlock,
ProjectIndexBlock,
)
from .forms import DonateForm
from .menu import MenuMixin
from .utils import get_donated_amount_from_api
class DonateFormMixin(models.Model):
"""Pages which has donate form. Must be in class definition before Page!"""
portal_project_id = models.IntegerField(
"ID projektu v darovacím portálu", blank=True, null=True
)
class Meta:
abstract = True
def serve(self, request, *args, **kwargs):
if request.method == "POST":
form = DonateForm(request.POST)
if form.is_valid():
url = form.get_redirect_url()
return redirect(url)
return super().serve(request, *args, **kwargs)
@property
def show_donate_form(self):
return bool(self.portal_project_id)
class DonateFormAmountsMixin(models.Model):
"""Amounts setup for donate forms."""
FIRST = 1
SECOND = 2
THIRD = 3
FOURTH = 4
FORM_CHOICES = [
(FIRST, "první"),
(SECOND, "druhá"),
(THIRD, "třetí"),
(FOURTH, "čtvrtá"),
]
form_amount_1 = models.IntegerField("pevná částka 1", default=100)
form_amount_2 = models.IntegerField("pevná částka 2", default=200)
form_amount_3 = models.IntegerField("pevná částka 3", default=500)
form_amount_4 = models.IntegerField("pevná částka 4", default=1000)
form_preselected = models.IntegerField(
"výchozí částka", default=FIRST, choices=FORM_CHOICES
)
form_monthly_amount_1 = models.IntegerField("měsíční částka 1", default=100)
form_monthly_amount_2 = models.IntegerField("měsíční částka 2", default=200)
form_monthly_amount_3 = models.IntegerField("měsíční částka 3", default=500)
form_monthly_amount_4 = models.IntegerField("měsíční částka 4", default=1000)
form_monthly_preselected = models.IntegerField(
"výchozí měsíční částka", default=FIRST, choices=FORM_CHOICES
)
class Meta:
abstract = True
class DonateHomePage(
MenuMixin,
DonateFormMixin,
DonateFormAmountsMixin,
Page,
ExtendedMetadataHomePageMixin,
MetadataPageMixin,
):
### FIELDS
# lead section
lead_title = models.CharField("hlavní nadpis", max_length=250, blank=True)
lead_body = RichTextField("hlavní popis", blank=True)
lead_video = models.URLField("video na youtube", blank=True, null=True)
lead_preview = models.ForeignKey(
"wagtailimages.Image",
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name="náhled videa",
)
# main section
content_blocks = StreamField(
[
("project_index", ProjectIndexBlock()),
("district_donation", DistrictDonationBlock()),
("party_support_form", PartySupportFormBlock()),
("custom", CustomContentBlock()),
],
blank=True,
use_json_field=True,
verbose_name="Obsah",
)
# settings
faq_page = models.ForeignKey(
"donate.DonateTextPage",
on_delete=models.PROTECT,
blank=True,
null=True,
related_name="FAQ",
verbose_name="Stránka s FAQ",
)
custom_links = StreamField(
[("custom_link", CustomLinkBlock(label="Vlastní odkaz"))],
verbose_name="Vlastní odkazy",
blank=True,
use_json_field=True,
)
facebook = models.URLField("Facebook URL", blank=True, null=True)
instagram = models.URLField("Instagram URL", blank=True, null=True)
twitter = models.URLField("Twitter URL", blank=True, null=True)
flickr = models.URLField("Flickr URL", blank=True, null=True)
matomo_id = models.IntegerField(
"Matomo ID pro sledování návštěvnosti", blank=True, null=True
)
### PANELS
content_panels = Page.content_panels + [
MultiFieldPanel(
[
FieldPanel("lead_title"),
FieldPanel("lead_body"),
FieldPanel("lead_video"),
FieldPanel("lead_preview"),
],
"hlavní sekce",
),
FieldPanel("content_blocks"),
]
promote_panels = make_promote_panels(admin_help.build(admin_help.IMPORTANT_TITLE))
settings_panels = [
MultiFieldPanel(
[FieldPanel("custom_links")],
"vlastní odkazy",
),
MultiFieldPanel(
[
FieldPanel("facebook"),
FieldPanel("instagram"),
FieldPanel("twitter"),
FieldPanel("flickr"),
],
"sociální sítě",
),
FieldPanel("matomo_id"),
FieldPanel("title_suffix"),
MultiFieldPanel(
[
FieldPanel("portal_project_id"),
FieldPanel("form_amount_1"),
FieldPanel("form_amount_2"),
FieldPanel("form_amount_3"),
FieldPanel("form_amount_4"),
FieldPanel("form_preselected"),
FieldPanel("form_monthly_amount_1"),
FieldPanel("form_monthly_amount_2"),
FieldPanel("form_monthly_amount_3"),
FieldPanel("form_monthly_amount_4"),
FieldPanel("form_monthly_preselected"),
],
"nastavení darů",
),
FieldPanel("faq_page"),
]
### 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 = [
"donate.DonateRegionIndexPage",
"donate.DonateProjectIndexPage",
"donate.DonateInfoPage",
"donate.DonateTextPage",
]
### OTHERS
# flag for rendering anchor links in menu
is_home = True
class Meta:
verbose_name = "Dary"
@property
def root_page(self):
return self
def get_404_response(self, request):
return HttpResponseRedirect(self.full_url)
@cached_property
def info_page_url(self):
return get_subpage_url(self, DonateInfoPage)
@cached_property
def project_indexes(self):
return DonateProjectIndexPage.objects.child_of(self).live()
@cached_property
def regions_page_url(self):
return get_subpage_url(self, DonateRegionIndexPage)
@cached_property
def has_projects(self):
return self.get_descendants().type(DonateProjectPage).live().exists()
def get_context(self, request):
context = super().get_context(request)
context["regions"] = (
self.get_descendants().type(DonateRegionPage).live().specific()
)
return context
class DonateRegionIndexPage(
Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin
):
### PANELS
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["donate.DonateHomePage"]
subpage_types = ["donate.DonateRegionPage"]
### OTHERS
# flag for rendering anchor links in menu
is_home = False
class Meta:
verbose_name = "Přehled krajů"
def get_context(self, request):
context = super().get_context(request)
context["regions"] = self.get_children().live().specific()
return context
class DonateRegionPage(
DonateFormMixin,
DonateFormAmountsMixin,
Page,
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
):
### FIELDS
main_title = models.CharField("hlavní nadpis na stránce", max_length=250)
body = RichTextField("obsah")
### PANELS
content_panels = Page.content_panels + [
FieldPanel("main_title"),
FieldPanel("body"),
]
promote_panels = make_promote_panels(
admin_help.build(
"Pokud není zadán <strong>Titulek stránky</strong>, použije "
"se <strong>Hlavní nadpis</strong> (tab obsah).",
admin_help.NO_SEARCH_IMAGE,
)
)
settings_panels = [
MultiFieldPanel(
[
FieldPanel("portal_project_id"),
FieldPanel("form_amount_1"),
FieldPanel("form_amount_2"),
FieldPanel("form_amount_3"),
FieldPanel("form_amount_4"),
FieldPanel("form_preselected"),
FieldPanel("form_monthly_amount_1"),
FieldPanel("form_monthly_amount_2"),
FieldPanel("form_monthly_amount_3"),
FieldPanel("form_monthly_amount_4"),
FieldPanel("form_monthly_preselected"),
],
"nastavení darů",
),
]
### RELATIONS
parent_page_types = ["donate.DonateRegionIndexPage"]
subpage_types = ["donate.DonateTargetedDonationsPage"]
### OTHERS
# flag for rendering anchor links in menu
is_home = False
class Meta:
verbose_name = "Kraj"
@cached_property
def targeted_donations_page_url(self):
return get_subpage_url(self, DonateTargetedDonationsPage)
@cached_property
def has_targeted_donations(self):
return self.get_descendants().type(DonateTargetedDonationsPage).live().exists()
class DonateProjectIndexPage(
Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin
):
### FIELDS
heading = models.CharField("Hlavní nadpis", max_length=32)
support_heading = models.CharField("Podpoř projekt nadpis", max_length=32)
support_description = RichTextField("Podpoř projekt popis")
### PANELS
content_panels = Page.content_panels + [
FieldPanel("heading"),
MultiFieldPanel(
[
FieldPanel("support_heading"),
FieldPanel("support_description"),
],
"Informace v sekci 'podpoř projekt' na homepage",
),
]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = ["donate.DonateHomePage"]
subpage_types = ["donate.DonateProjectPage"]
### OTHERS
# flag for rendering anchor links in menu
is_home = False
@property
def projects(self):
return (
DonateProjectPage.objects.child_of(self)
.filter()
.distinct()
.order_by("-is_sticky", "-date")
.live()
)[:3]
class Meta:
verbose_name = "Přehled projektů"
def get_context(self, request):
context = super().get_context(request)
paginator = Paginator(
self.get_children().live().specific().order_by("-donateprojectpage__date"),
6,
)
page = request.GET.get("page")
try:
projects = paginator.page(page)
except PageNotAnInteger:
projects = paginator.page(1)
except EmptyPage:
projects = paginator.page(paginator.num_pages)
context["projects"] = projects
return context
class DonateProjectPage(
DonateFormMixin,
DonateFormAmountsMixin,
Page,
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
):
TITLE_PROJECT = "Daruj na projekt"
### FIELDS
date = models.DateField("Běží od")
until = models.DateField("Běží do", null=True, blank=True)
perex = models.TextField("Krátký popis")
body = RichTextField("Obsah")
is_new = models.BooleanField('Označení "nový projekt"', default=False)
is_sticky = models.BooleanField(
"Je připnutý",
help_text="Pokud je projekt připnutý, na domovské stránce se v seznamech projektů udrží na začátku.",
default=False,
)
allow_periodic_donations = models.BooleanField(
"Umožnit pravidelné dary", default=False
)
photo = models.ForeignKey(
"wagtailimages.Image",
verbose_name="Fotka",
on_delete=models.PROTECT,
null=True,
blank=True,
)
gallery = StreamField(
[("photo", ImageChooserBlock(label="fotka"))],
verbose_name="galerie fotek",
blank=True,
use_json_field=True,
)
form_title = models.CharField(
"Titulek formuláře",
help_text="Např. 'Daruj na projekt', 'Daruj na kampaň', ...",
max_length=32,
default=TITLE_PROJECT,
)
expected_amount = models.IntegerField("Očekávaná částka", blank=True, null=True)
donated_amount = models.IntegerField("Vybraná částka", blank=True, null=True)
coalition_design = models.BooleanField("Koaliční design", default=False)
# we will use photo as search image
search_image = None
crowdfunding = StreamField(
[("reward_block", CrowdfundingRewardBlock())],
verbose_name="Crowdfunding bloky",
blank=True,
use_json_field=True,
)
### PANELS
content_panels = Page.content_panels + [
MultiFieldPanel(
[
FieldPanel("is_new"),
FieldPanel("is_sticky"),
FieldPanel("perex"),
FieldPanel("photo"),
],
"Info do přehledu projektů",
),
MultiFieldPanel(
[
FieldPanel("date"),
FieldPanel("until"),
],
"Časový interval projektu",
),
FieldPanel("body"),
FieldPanel("gallery"),
]
promote_panels = make_promote_panels(
admin_help.build(
"Pokud není zadán <strong>Titulek stránky</strong>, použije "
"se „Podpoř projekt <strong>Název</strong>“ (tab obsah).",
admin_help.NO_DESCRIPTION_USE_PEREX,
),
search_image=False,
)
settings_panels = [
PublishingPanel(),
MultiFieldPanel(
[
FieldPanel("form_title"),
FieldPanel("expected_amount"),
FieldPanel("portal_project_id"),
FieldPanel("allow_periodic_donations"),
FieldPanel("form_amount_1"),
FieldPanel("form_amount_2"),
FieldPanel("form_amount_3"),
FieldPanel("form_amount_4"),
FieldPanel("form_preselected"),
FieldPanel("form_monthly_amount_1"),
FieldPanel("form_monthly_amount_2"),
FieldPanel("form_monthly_amount_3"),
FieldPanel("form_monthly_amount_4"),
FieldPanel("form_monthly_preselected"),
],
"nastavení darů",
),
MultiFieldPanel(
[
FieldPanel("crowdfunding"),
],
"Nastavení crowdfundingových odměn",
),
FieldPanel("coalition_design"),
]
### RELATIONS
parent_page_types = ["donate.DonateProjectIndexPage"]
subpage_types = ["donate.DonateSecretPreviewPage"]
### OTHERS
# flag for rendering anchor links in menu
is_home = False
class Meta:
verbose_name = "Projekt"
def get_meta_image(self):
return self.photo
def get_meta_description(self):
if self.search_description:
return self.search_description
if len(self.perex) > 150:
return str(self.perex)[:150] + "..."
return self.perex
def get_donated_amount(self):
if self.portal_project_id is None:
return 0
# instance caching for multiple method calls during one request
if not hasattr(self, "_donated_amount"):
# cache portal API calls (defaults to 5 min)
key = f"donated_amount_{self.portal_project_id}"
amount = cache.get(key)
if amount is None:
amount = get_donated_amount_from_api(self.portal_project_id)
if amount is not None:
# save amount into database to be used if next API calls fails
self.donated_amount = amount
self.save()
cache.set(key, amount, settings.DONATE_PORTAL_API_CACHE_TIMEOUT)
self._donated_amount = self.donated_amount or 0
return self._donated_amount
@property
def donated_percentage(self):
if not self.expected_amount:
return 0
if self.get_donated_amount() >= self.expected_amount:
return 100
return round(self.get_donated_amount() / self.expected_amount * 100)
def get_context(self, request):
context = super().get_context(request)
context["other_projects"] = (
self.get_siblings(inclusive=False)
.live()
.specific()
.order_by("-donateprojectpage__date")[:3]
)
return context
def get_template(self, request, *args, **kwargs):
if self.coalition_design:
return "donate/donate_project_page_coalition.html"
return super().get_template(request, *args, **kwargs)
class DonateTextPage(Page, ExtendedMetadataPageMixin, SubpageMixin, MetadataPageMixin):
### FIELDS
body = RichTextField("obsah", blank=True)
### PANELS
content_panels = Page.content_panels + [FieldPanel("body")]
promote_panels = make_promote_panels()
settings_panels = []
### RELATIONS
parent_page_types = [
"donate.DonateHomePage",
"donate.DonateTextPage",
"donate.DonateInfoPage",
]
subpage_types = ["donate.DonateTextPage", "donate.DonateInfoPage"]
### OTHERS
# flag for rendering anchor links in menu
is_home = False
class Meta:
verbose_name = "Stránka s textem"
class DonateInfoPage(
DonateFormMixin,
DonateFormAmountsMixin,
Page,
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
):
### FIELDS
body = RichTextField("obsah", blank=True)
### PANELS
content_panels = Page.content_panels + [FieldPanel("body")]
promote_panels = make_promote_panels()
settings_panels = [
PublishingPanel(),
MultiFieldPanel(
[
FieldPanel("form_amount_1"),
FieldPanel("form_amount_2"),
FieldPanel("form_amount_3"),
FieldPanel("form_amount_4"),
FieldPanel("form_preselected"),
FieldPanel("form_monthly_amount_1"),
FieldPanel("form_monthly_amount_2"),
FieldPanel("form_monthly_amount_3"),
FieldPanel("form_monthly_amount_4"),
FieldPanel("form_monthly_preselected"),
],
"nastavení darů",
),
]
### RELATIONS
parent_page_types = [
"donate.DonateHomePage",
"donate.DonateTextPage",
"donate.DonateInfoPage",
]
subpage_types = ["donate.DonateTextPage", "donate.DonateInfoPage"]
### OTHERS
# flag for rendering anchor links in menu
is_home = False
class Meta:
verbose_name = "Infostránka s formulářem"
# use portal_project_id from home page
@cached_property
def portal_project_id(self):
return self.get_parent().specific.portal_project_id
class TargetedDonation(Orderable):
page = ParentalKey(
"donate.DonateTargetedDonationsPage",
on_delete=models.CASCADE,
related_name="targeted_donations",
)
is_main = models.BooleanField(
"hlavní dar", default=False, help_text="zobrazené samostatně nahoře"
)
title = models.CharField("název", max_length=255)
description = models.CharField(
"popis",
null=True,
blank=True,
max_length=255,
help_text="zobrazí se jen u hlavních darů",
)
portal_project_id = models.IntegerField("ID projektu v darovacím portálu")
panels = [
FieldPanel("portal_project_id"),
FieldPanel("title"),
FieldPanel("description"),
FieldPanel("is_main"),
]
class DonateTargetedDonationsPage(
DonateFormMixin,
DonateFormAmountsMixin,
Page,
ExtendedMetadataPageMixin,
SubpageMixin,
MetadataPageMixin,
):
### FIELDS
# page does not have specific portal_project_id
portal_project_id = None
### PANELS
content_panels = Page.content_panels + [
MultiFieldPanel([InlinePanel("targeted_donations")], "adresné dary"),
]
promote_panels = make_promote_panels()
settings_panels = [
PublishingPanel(),
MultiFieldPanel(
[
FieldPanel("form_amount_1"),
FieldPanel("form_amount_2"),
FieldPanel("form_amount_3"),
FieldPanel("form_amount_4"),
FieldPanel("form_preselected"),
FieldPanel("form_monthly_amount_1"),
FieldPanel("form_monthly_amount_2"),
FieldPanel("form_monthly_amount_3"),
FieldPanel("form_monthly_amount_4"),
FieldPanel("form_monthly_preselected"),
],
"nastavení darů",
),
]
### RELATIONS
parent_page_types = ["donate.DonateRegionPage"]
subpage_types = []
### OTHERS
# flag for rendering anchor links in menu
is_home = False
class Meta:
verbose_name = "Adresné dary"
def get_context(self, request):
context = super().get_context(request)
try:
selected_project_id = int(request.GET.get("p", 0))
selected_target = self.targeted_donations.get(
portal_project_id=selected_project_id
)
except (ValueError, TargetedDonation.DoesNotExist):
selected_target = None
if selected_target:
context["main_targets"] = [selected_target]
context["other_targets"] = []
context["is_preselected"] = True
else:
context["main_targets"] = self.targeted_donations.filter(is_main=True)
context["other_targets"] = self.targeted_donations.filter(is_main=False)
context["is_preselected"] = False
if context["main_targets"]:
context["initial_project_id"] = context["main_targets"][0].portal_project_id
elif context["other_targets"]:
context["initial_project_id"] = context["other_targets"][
0
].portal_project_id
else:
context["initial_project_id"] = 0
return context
class DonateSecretPreviewPage(Page):
max_count_per_parent = 1
parent_page_types = [
"donate.DonateProjectPage",
]
subpage_types = []
class Meta:
verbose_name = "Skrytá stránka pro náhled konceptu"
def get_context(self, request, *args, **kwargs):
parent_page = self.get_parent().get_latest_revision_as_object()
context = parent_page.get_context(request=request)
context.update({"disable_robots": True})
return context
def get_template(self, request, *args, **kwargs):
parent_page = self.get_parent().get_latest_revision_as_object()
return parent_page.get_template(request, *args, **kwargs)
def serve(self, request, *args, **kwargs):
return TemplateResponse(
request,
self.get_template(request, *args, **kwargs),
self.get_context(request, *args, **kwargs),
)