Skip to content
Snippets Groups Projects
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),
        )