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 = "project" TITLE_CAMPAIGN = "camapign" FORM_TITLE_CHOICES = ( (TITLE_PROJECT, "Podpoř projekt"), (TITLE_CAMPAIGN, "Podpoř kampaň"), ) ### 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", max_length=10, choices=FORM_TITLE_CHOICES, 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), )