Files
neuf-www/dnscms/events/models.py
T

526 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django import forms
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Min, Prefetch, Q, UniqueConstraint
from django.utils import timezone
from django.utils.html import mark_safe
from django.utils.translation import gettext_lazy as _
from grapple.helpers import register_query_field, register_singular_query_field
from grapple.models import (
GraphQLBoolean,
GraphQLCollection,
GraphQLForeignKey,
GraphQLImage,
GraphQLRichText,
GraphQLStreamfield,
GraphQLString,
)
from modelcluster.fields import ParentalKey, ParentalManyToManyField
from modelcluster.models import ClusterableModel
from wagtail.admin.panels import (
FieldPanel,
FieldRowPanel,
HelpPanel,
InlinePanel,
MultiFieldPanel,
MultipleChooserPanel,
TitleFieldPanel,
)
from wagtail.fields import RichTextField
from wagtail.models import Orderable, Page, PageManager, PageQuerySet
from wagtail.search import index
from wagtail.snippets.models import register_snippet
from wagtail_headless_preview.models import HeadlessMixin
from associations.widgets import AssociationChooserWidget
from dnscms.fields import CommonStreamField
from dnscms.options import ALL_PIGS
from dnscms.wordpress.models import (
DeferWPFieldsManagerMixin,
WPAwareManager,
WPImportedPageMixin,
)
from venues.models import VenuePage
@register_singular_query_field("eventIndex")
class EventIndex(HeadlessMixin, Page):
max_count = 1
subpage_types = ["events.EventPage"]
def future_events(self, info, **kwargs):
return (
EventPage.objects.live()
.future()
.order_by("next_occurrence")
.select_related("featured_image")
.prefetch_related(
Prefetch(
"occurrences",
queryset=EventOccurrence.objects.select_related("venue"),
),
"categories",
Prefetch(
"organizers",
queryset=EventOrganizer.objects.select_related("association"),
),
)
)
graphql_fields = [
GraphQLCollection(
GraphQLForeignKey,
"future_events",
"events.EventPage",
required=True,
item_required=True,
is_queryset=True,
),
]
search_fields = []
@register_snippet
@register_query_field("eventCategory", "eventCategories")
class EventCategory(models.Model):
name = models.CharField(
max_length=100,
null=False,
blank=False,
)
slug = models.SlugField(
verbose_name=_("slug"),
max_length=255,
help_text=_("The name of the category as it will appear in URLs."),
)
show_in_filters = models.BooleanField(
default=False,
help_text=_("Should this category be available as a filter in the event programme?"),
)
PIG_CHOICES = [
("", _("None")),
] + ALL_PIGS
pig = models.CharField(
max_length=32,
choices=PIG_CHOICES,
default="",
blank=True,
help_text=_("Default pig for events of this kind."),
)
panels = [
TitleFieldPanel("name"),
FieldPanel("slug"),
FieldPanel("show_in_filters"),
FieldPanel("pig", heading=_("Pig")),
]
graphql_fields = [
GraphQLString("name", required=True),
GraphQLString("slug", required=True),
GraphQLBoolean("show_in_filters", required=True),
GraphQLString("pig", required=True),
]
class Meta:
verbose_name = _("event category")
verbose_name_plural = _("event categories")
ordering = ["name"]
def __str__(self):
return self.name
class EventOrganizerLink(Orderable):
event = ParentalKey(
"events.EventPage", on_delete=models.CASCADE, related_name="organizer_links"
)
organizer = models.ForeignKey(
"events.EventOrganizer",
on_delete=models.PROTECT,
related_name="organized_events",
help_text="",
)
panels = [FieldPanel("organizer", heading="", help_text="")]
graphql_fields = [
GraphQLForeignKey("organizer", "events.EventOrganizer"),
]
def __str__(self):
return f"{self.organizer.name}"
class Meta:
verbose_name = _("organizer")
verbose_name_plural = _("organizers")
constraints = [
UniqueConstraint(
"event", "organizer", name="event_organizer_link_event_organizer_unique"
)
]
@register_snippet
@register_query_field("eventOrganizer", "eventOrganizers")
class EventOrganizer(index.Indexed, ClusterableModel):
objects = WPAwareManager()
name = models.CharField(
max_length=100,
null=False,
blank=False,
)
slug = models.SlugField(
verbose_name=_("slug"),
max_length=255,
help_text=_("The name of the organizer as it will appear in URLs."),
)
association = models.ForeignKey(
"associations.AssociationPage",
null=True,
blank=True,
on_delete=models.PROTECT,
related_name="organizers",
help_text=_("If a DNS association or committee is behind it, choose it here."),
)
external_url = models.URLField(
blank=True,
max_length=512,
help_text=_("Link to the external organizer's website"),
)
panels = [
TitleFieldPanel("name"),
FieldPanel("slug"),
FieldPanel(
"association",
widget=AssociationChooserWidget(linked_fields={"association": "#id_association"}),
heading=_("Internal organizer"),
),
MultiFieldPanel(
heading=_("External organizer"),
children=[
FieldPanel(
"external_url",
heading=_("Website"),
help_text=_("Leave this empty if the organizer exists in the list above."),
),
],
),
]
graphql_fields = [
GraphQLString("name", required=True),
GraphQLString("slug", required=True),
GraphQLForeignKey("association", "associations.AssociationPage", required=False),
GraphQLString("external_url"),
]
search_fields = [
index.SearchField("name"),
index.AutocompleteField("name"),
]
class Meta:
verbose_name = _("event organizer")
verbose_name_plural = _("event organizers")
ordering = ["name"]
def __str__(self):
return self.name
class EventPageQuerySet(PageQuerySet):
def future(self):
now = timezone.now()
today_start = timezone.localtime(now).replace(hour=0, minute=0, second=0, microsecond=0)
next_occurrence = Min("occurrences__start", filter=Q(occurrences__start__gte=today_start))
return self.filter(occurrences__start__gte=today_start).annotate(
next_occurrence=next_occurrence
)
class EventPageManager(
DeferWPFieldsManagerMixin,
PageManager.from_queryset(EventPageQuerySet),
):
pass
class EventPage(HeadlessMixin, WPImportedPageMixin, Page):
subpage_types = []
parent_page_types = ["events.EventIndex"]
show_in_menus = False
objects = EventPageManager()
featured_image = models.ForeignKey(
"images.CustomImage",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="+",
help_text=_(
"Choose an image for use in the programme and other surfaces. "
"Should be a photo or an illustration without too much text "
" don't reuse a Facebook cover uncritically!"
),
)
subtitle = models.CharField(
blank=True,
max_length=128,
help_text=_(
"A short text that appears right below the title. "
"Feel free to leave it empty if you fit most of it in the main title."
),
)
lead = RichTextField(features=["italic", "link"], blank=True)
body = CommonStreamField
categories = ParentalManyToManyField(
"events.EventCategory",
blank=True,
)
organizers = models.ManyToManyField(
"events.EventOrganizer", through="events.EventOrganizerLink"
)
PIG_CHOICES = [
("", _("None")),
("automatic", _("Automatic")),
] + ALL_PIGS
pig = models.CharField(
max_length=32,
choices=PIG_CHOICES,
default="automatic",
blank=True,
help_text=_(
"The pig that hangs out on the event page. "
"Automatic causes one to be chosen based on the event's category."
),
)
ticket_url = models.URLField(
blank=True,
max_length=1024,
help_text=_("Direct link to ticket purchase, e.g. TicketCo, Billetto or Ticketmaster"),
)
facebook_url = models.URLField(
blank=True,
max_length=1024,
help_text=_("Direct link to the event on Facebook"),
)
free = models.BooleanField(null=False, default=False)
price_regular = models.CharField(max_length=32, blank=True)
price_student = models.CharField(max_length=32, blank=True)
price_member = models.CharField(max_length=32, blank=True)
ticket_panels = [
FieldPanel("free", heading=_("Free"), help_text=_("Is this event free for everyone?")),
MultiFieldPanel(
children=[
FieldRowPanel(
children=[
FieldPanel("price_regular", heading=_("Regular price")),
FieldPanel("price_student", heading=_("Price for students")),
FieldPanel("price_member", heading=_("Price for DNS members")),
],
help_text="",
),
HelpPanel(
content=mark_safe(
_(
"Write <strong>0</strong> for free. "
"An empty field hides the price category. "
"If possible, write digits only."
)
)
),
],
attrs={"id": "specific_pricing_panel"},
),
FieldPanel("ticket_url", heading=_("Ticket purchase link")),
]
content_panels = Page.content_panels + [
FieldPanel("subtitle", heading=_("Subtitle")),
FieldPanel("featured_image"),
FieldPanel("lead", heading=_("Lead")),
FieldPanel("body"),
FieldPanel("categories", widget=forms.CheckboxSelectMultiple),
MultiFieldPanel(
heading=_("Organizers"),
children=[
HelpPanel(
content=_("Who is behind the event?"),
),
MultipleChooserPanel(
"organizer_links", chooser_field_name="organizer", label=_("Organizer")
),
],
),
FieldPanel("pig", heading=_("Pig")),
FieldPanel(
"facebook_url",
heading=_("Facebook link"),
help_text=_("Direct link to the event on Facebook."),
),
MultiFieldPanel(heading=_("Pricing and tickets"), children=ticket_panels),
MultiFieldPanel(
heading=_("Date, time and venue"),
children=[
HelpPanel(
content=_(
"If the event spans several days, add each day as a separate occurrence."
),
),
InlinePanel("occurrences", min_num=1, label=_("Occurrence")),
],
),
]
graphql_fields = [
GraphQLString("subtitle"),
GraphQLImage("featured_image"),
GraphQLRichText("lead"),
GraphQLStreamfield("body"),
GraphQLString("pig", required=True),
GraphQLString("ticket_url"),
GraphQLString("facebook_url"),
GraphQLBoolean("free"),
GraphQLString("price_regular"),
GraphQLString("price_student"),
GraphQLString("price_member"),
GraphQLCollection(
GraphQLForeignKey,
"categories",
"events.EventCategory",
required=True,
item_required=True,
),
GraphQLCollection(
GraphQLForeignKey,
"occurrences",
"events.EventOccurrence",
required=True,
item_required=True,
),
GraphQLCollection(
GraphQLForeignKey,
"organizers",
"events.EventOrganizer",
required=True,
item_required=True,
),
]
search_fields = Page.search_fields + [index.SearchField("body")]
class Meta:
verbose_name = _("event")
verbose_name_plural = _("events")
def clean(self):
super().clean()
# remove duped organizers based on name
organizers = [x.organizer.name for x in self.organizer_links.all()]
if len(organizers) != len(set(organizers)):
seen_organizers = set()
for link in self.organizer_links.all():
if link.organizer.name in seen_organizers:
self.organizer_links.remove(link)
else:
seen_organizers.add(link.organizer.name)
# if the event is free, all specific pricing is unset
if self.free:
self.price_regular = ""
self.price_student = ""
self.price_member = ""
class EventOccurrence(Orderable):
objects = WPAwareManager()
event = ParentalKey(EventPage, on_delete=models.CASCADE, related_name="occurrences")
start = models.DateTimeField()
end = models.DateTimeField(null=True, blank=True)
venue = models.ForeignKey(
VenuePage,
on_delete=models.PROTECT,
related_name="event_occurrences",
blank=True,
null=True,
)
venue_custom = models.CharField(
blank=True,
max_length=128,
help_text=mark_safe(
_(
"Use this <em>if none of the venues that can be selected on the left</em> fit. "
"E.g. <em>Frederikkeplassen</em> or <em>Sirkusteltet</em>."
)
),
)
panels = [
FieldRowPanel(
children=[
FieldPanel("start", heading=_("Start")),
FieldPanel("end", heading=_("End")),
],
),
FieldRowPanel(
children=[
FieldPanel("venue", heading=_("Venue"), widget=forms.Select),
FieldPanel("venue_custom", heading=_("Venue as free text")),
],
),
]
graphql_fields = [
GraphQLString("start", required=True),
GraphQLString("end"),
GraphQLForeignKey("venue", "venues.VenuePage"),
GraphQLString("venue_custom"),
]
def clean(self):
if self.venue_custom:
trimmed = self.venue_custom.strip()
self.venue_custom = trimmed
if trimmed:
match = VenuePage.objects.filter(title=trimmed).first()
if match:
self.venue = match
self.venue_custom = ""
if self.venue and self.venue_custom:
raise ValidationError(
{
"venue_custom": _(
"You can't both pick a venue and write something in this field."
)
}
)
if not self.venue and not self.venue_custom:
raise ValidationError({"venue": _("Venue is required.")})
def __str__(self):
return f"{self.start}--{self.end}"
class Meta:
verbose_name = _("occurrence")
verbose_name_plural = _("occurrences")