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.shortcuts import redirect from django.utils.translation import gettext_lazy from modelcluster.fields import ParentalKey from wagtail.admin.edit_handlers import ( FieldPanel, HelpPanel, InlinePanel, MultiFieldPanel, PublishingPanel, StreamFieldPanel, ) from wagtail.core import blocks from wagtail.core.fields import RichTextField, StreamField from wagtail.core.models import Orderable, Page from wagtail.images.blocks import ImageChooserBlock from wagtail.images.edit_handlers import ImageChooserPanel from wagtailmetadata.models import MetadataPageMixin from tuning import help from .forms import DonateForm from .utils import get_donated_amount_from_api class SubpageMixin: """Must be used in class definition before MetadataPageMixin!""" # flag for rendering anchor links in menu is_home = False @property def root_page(self): if not hasattr(self, "_root_page"): self._root_page = self.get_ancestors().type(DonateHomePage).specific().get() return self._root_page def get_meta_image(self): return self.search_image or self.root_page.get_meta_image() 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): if request.method == "POST": form = DonateForm(request.POST) if form.is_valid(): url = form.get_redirect_url() return redirect(url) return super().serve(request) @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 FORM_CHOICES = [ (FIRST, "první"), (SECOND, "druhá"), (THIRD, "třetí"), ] 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_preselected = models.IntegerField( "výchozí částka", default=FIRST, choices=FORM_CHOICES ) class Meta: abstract = True def get_url(page, dest_page_type): try: return page.get_children().type(dest_page_type).live().first().get_url() except (Page.DoesNotExist, AttributeError): return "#" class DonateHomePage(DonateFormMixin, DonateFormAmountsMixin, Page, MetadataPageMixin): ### FIELDS # lead section lead_title = models.CharField("hlavní nadpis", max_length=250, blank=True) lead_body = models.TextField("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", ) # support section support_title = models.CharField("podpoř stranu nadpis", max_length=250, blank=True) support_body = models.TextField("podpoř stranu popis", blank=True) # projects section project_title = models.CharField( "podpoř projekt nadpis", max_length=250, blank=True ) project_body = models.TextField("podpoř projekt popis", blank=True) # regions section region_title = models.CharField("podpoř kraj nadpis", max_length=250, blank=True) region_body = models.TextField("podpoř kraj popis", blank=True) # settings 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"), ImageChooserPanel("lead_preview"), ], "hlavní sekce", ), MultiFieldPanel( [FieldPanel("support_title"), FieldPanel("support_body")], "podpoř stranu", ), MultiFieldPanel( [FieldPanel("project_title"), FieldPanel("project_body")], "podpoř projekt", ), MultiFieldPanel( [FieldPanel("region_title"), FieldPanel("region_body")], "podpoř kraj", ), ] promote_panels = [ MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("search_image"), HelpPanel(help.build(help.IMPORTANT_TITLE)), ], gettext_lazy("Common page configuration"), ), ] settings_panels = [ MultiFieldPanel( [ FieldPanel("facebook"), FieldPanel("instagram"), FieldPanel("twitter"), FieldPanel("flickr"), ], "sociální sítě", ), FieldPanel("matomo_id"), MultiFieldPanel( [ FieldPanel("portal_project_id"), FieldPanel("form_amount_1"), FieldPanel("form_amount_2"), FieldPanel("form_amount_3"), FieldPanel("form_preselected"), ], "nastavení darů", ), ] ### 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 @property def info_page_url(self): return get_url(self, DonateInfoPage) @property def projects_page_url(self): return get_url(self, DonateProjectIndexPage) @property def regions_page_url(self): return get_url(self, DonateRegionIndexPage) @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() ) context["projects"] = ( self.get_descendants() .type(DonateProjectPage) .live() .specific() .order_by("-donateprojectpage__date")[:3] ) return context class DonateRegionIndexPage(Page, SubpageMixin, MetadataPageMixin): ### PANELS promote_panels = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("search_image"), HelpPanel(help.build(help.NO_SEO_TITLE, help.NO_SEARCH_IMAGE)), ], gettext_lazy("Common page configuration"), ), ] settings_panels = [] ### RELATIONS parent_page_types = ["donate.DonateHomePage"] subpage_types = ["donate.DonateRegionPage"] ### OTHERS 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, 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", classname="full"), ] promote_panels = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("search_image"), HelpPanel( help.build( "Pokud není zadán <strong>Titulek stránky</strong>, použije " "se <strong>Hlavní nadpis</strong> (tab obsah).", help.NO_SEARCH_IMAGE, ) ), ], gettext_lazy("Common page configuration"), ), ] settings_panels = [ MultiFieldPanel( [ FieldPanel("portal_project_id"), FieldPanel("form_amount_1"), FieldPanel("form_amount_2"), FieldPanel("form_amount_3"), FieldPanel("form_preselected"), ], "nastavení darů", ), ] ### RELATIONS parent_page_types = ["donate.DonateRegionIndexPage"] subpage_types = ["donate.DonateTargetedDonationsPage"] ### OTHERS class Meta: verbose_name = "Kraj" def get_meta_title(self): return self.seo_title or self.main_title @property def targeted_donations_page_url(self): return get_url(self, DonateTargetedDonationsPage) @property def has_targeted_donations(self): return self.get_descendants().type(DonateTargetedDonationsPage).live().exists() class DonateProjectIndexPage(Page, SubpageMixin, MetadataPageMixin): ### PANELS promote_panels = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("search_image"), HelpPanel(help.build(help.NO_SEO_TITLE, help.NO_SEARCH_IMAGE)), ], gettext_lazy("Common page configuration"), ), ] settings_panels = [] ### RELATIONS parent_page_types = ["donate.DonateHomePage"] subpage_types = ["donate.DonateProjectPage"] ### OTHERS 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, SubpageMixin, MetadataPageMixin ): ### FIELDS date = models.DateField("běží od") perex = models.TextField("krátký popis") body = RichTextField("obsah") is_new = models.BooleanField('označení "nový projekt"', 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, ) expected_amount = models.IntegerField("očekávaná částka", blank=True, null=True) donated_amount = models.IntegerField("vybraná částka", blank=True, null=True) # we will use photo as search image search_image = None ### PANELS content_panels = Page.content_panels + [ MultiFieldPanel( [FieldPanel("is_new"), FieldPanel("perex"), ImageChooserPanel("photo")], "info do přehledu projektů", ), FieldPanel("date"), FieldPanel("expected_amount"), FieldPanel("body", classname="full"), StreamFieldPanel("gallery"), ] promote_panels = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), HelpPanel( help.build( "Pokud není zadán <strong>Titulek stránky</strong>, použije " "se „Podpoř projekt <strong>Název</strong>“ (tab obsah).", "Pokud není zadán <strong>Popis vyhledávání</strong>, použije " "se prvních 150 znaků <strong>Perexu</strong> (tab obsah).", ) ), ], gettext_lazy("Common page configuration"), ), ] settings_panels = [ PublishingPanel(), MultiFieldPanel( [ FieldPanel("portal_project_id"), FieldPanel("allow_periodic_donations"), FieldPanel("form_amount_1"), FieldPanel("form_amount_2"), FieldPanel("form_amount_3"), FieldPanel("form_preselected"), ], "nastavení darů", ), ] ### RELATIONS parent_page_types = ["donate.DonateProjectIndexPage"] subpage_types = [] ### OTHERS class Meta: verbose_name = "Projekt" def get_meta_image(self): return self.photo def get_meta_title(self): return self.seo_title or self.title 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 class DonateTextPage(Page, SubpageMixin, MetadataPageMixin): ### FIELDS body = RichTextField("obsah", blank=True) ### PANELS content_panels = Page.content_panels + [ FieldPanel("body", classname="full"), ] promote_panels = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("search_image"), HelpPanel(help.build(help.NO_SEO_TITLE, help.NO_SEARCH_IMAGE)), ], gettext_lazy("Common page configuration"), ), ] settings_panels = [] ### RELATIONS parent_page_types = ["donate.DonateHomePage"] subpage_types = [] ### OTHERS class Meta: verbose_name = "Stránka s textem" class DonateInfoPage(DonateFormMixin, Page, SubpageMixin, MetadataPageMixin): ### FIELDS body = RichTextField("obsah", blank=True) ### PANELS content_panels = Page.content_panels + [ FieldPanel("body", classname="full"), ] promote_panels = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("search_image"), HelpPanel(help.build(help.NO_SEO_TITLE, help.NO_SEARCH_IMAGE)), ], gettext_lazy("Common page configuration"), ), ] settings_panels = [] ### RELATIONS parent_page_types = ["donate.DonateHomePage"] subpage_types = [] ### OTHERS class Meta: verbose_name = "Infostránka s formulářem" # use portal_project_id from home page @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, Page, 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 = [ MultiFieldPanel( [ FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("search_image"), HelpPanel(help.build(help.NO_SEO_TITLE, help.NO_SEARCH_IMAGE)), ], gettext_lazy("Common page configuration"), ), ] settings_panels = [] ### RELATIONS parent_page_types = ["donate.DonateRegionPage"] subpage_types = [] ### OTHERS 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