from django import forms from django.core.exceptions import ValidationError from django.db import models from django.db.models import Min, 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 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") 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="Skal denne kategorien være mulig å filtrere på i programmet?" ) PIG_CHOICES = [ ("", "Ingen"), ] + ALL_PIGS pig = models.CharField( max_length=32, choices=PIG_CHOICES, default="", blank=True, help_text="Standardgris for arrangementer av denne typen.", ) panels = [ TitleFieldPanel("name"), FieldPanel("slug"), FieldPanel("show_in_filters"), FieldPanel("pig", heading="Gris"), ] 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 = "Arrangør" verbose_name_plural = "Arrangører" constraints = [ UniqueConstraint( "event", "organizer", name="event_organizer_link_event_organizer_unique" ) ] @register_snippet @register_query_field("eventOrganizer", "eventOrganizers") class EventOrganizer(ClusterableModel): 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="Om en samfundsforening eller et utvalg står bak, velg det her.", ) external_url = models.URLField( blank=True, max_length=512, help_text="Lenke til nettstedet til ekstern arrangør", ) panels = [ TitleFieldPanel("name"), FieldPanel("slug"), FieldPanel( "association", widget=AssociationChooserWidget(linked_fields={"association": "#id_association"}), heading="Intern arrangør", ), MultiFieldPanel( heading="Ekstern arrangør", children=[ FieldPanel( "external_url", heading="Nettsted", help_text="La denne stå tom om arrangøren finnes i lista over.", ), ], ), ] graphql_fields = [ GraphQLString("name", required=True), GraphQLString("slug", required=True), GraphQLForeignKey("association", "associations.AssociationPage", required=False), GraphQLString("external_url"), ] 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 ) EventPageManager = PageManager.from_queryset(EventPageQuerySet) 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=( "Velg et bilde til bruk i programmet og andre visningsflater. " "Bør være et bilde eller en illustrasjon uten tekst " "– ikke gjenbruk et Facebook-cover ukritisk!" ), ) subtitle = models.CharField( blank=True, max_length=128, help_text=( "En kort tekst som kommer rett under under tittelen. " "La denne gjerne stå tom om du fikk plass til det meste i tittelen." ), ) 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 = [ ("", "Ingen"), ("automatic", "Automatisk"), ] + ALL_PIGS pig = models.CharField( max_length=32, choices=PIG_CHOICES, default="automatic", blank=True, help_text=( "Grisen som henger på arrangementssiden. " "Automatisk fører til at en velges basert på arrangementets kategori." ), ) ticket_url = models.URLField( blank=True, max_length=1024, help_text="Lenke direkte til billettkjøp, f.eks. TicketCo eller Ticketmaster", ) facebook_url = models.URLField( blank=True, max_length=1024, help_text="Lenke direkte til arrangementet på 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="Gratis", help_text="Er dette arrangementet gratis for alle?"), MultiFieldPanel( children=[ FieldRowPanel( children=[ FieldPanel("price_regular", heading="Ordinær pris"), FieldPanel("price_student", heading="Pris for studenter"), FieldPanel("price_member", heading="Pris for medlemmer av DNS"), ], help_text="", ), HelpPanel( content=mark_safe( "Skriv 0 om gratis. " "Tomt felt skjuler priskategorien. Om mulig, skriv kun tall." ) ), ], attrs={"id": "specific_pricing_panel"}, ), FieldPanel("ticket_url", heading="Billettkjøpslenke"), ] content_panels = Page.content_panels + [ FieldPanel("subtitle", heading="Undertittel"), FieldPanel("featured_image"), FieldPanel("lead", heading="Ingress"), FieldPanel("body"), FieldPanel("categories", widget=forms.CheckboxSelectMultiple), MultiFieldPanel( heading="Arrangører", children=[ HelpPanel( content=("Hvem står bak arrangementet?"), ), MultipleChooserPanel( "organizer_links", chooser_field_name="organizer", label="Arrangør" ), ], ), FieldPanel("pig", heading="Gris"), FieldPanel( "facebook_url", heading="Facebook-lenke", help_text="Lenke direkte til arrangementet på Facebook.", ), MultiFieldPanel(heading="Priser og billettkjøp", children=ticket_panels), MultiFieldPanel( heading="Dato, tid og lokale", children=[ HelpPanel( content=( "Om arrangementet går over flere dager, " "legg inn hver dag som en egen forekomst." ), ), InlinePanel("occurrences", min_num=1, label="Forekomst"), ], ), ] 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 = "" def import_wordpress_data(self, data): import datetime import html from zoneinfo import ZoneInfo from django.core.validators import URLValidator validate_url = URLValidator(schemes=["http", "https"]) def fix_url(url): if not url: return None url = url.strip() try: validate_url(url) except Exception: print(f"Bogus URL for {self.wp_post_id}: {url}") return None return url # Wagtail page model fields self.title = html.unescape(data["title"]) self.slug = data["slug"] self.first_published_at = data["first_published_at"] self.last_published_at = data["last_published_at"] self.latest_revision_created_at = data["latest_revision_created_at"] self.search_description = data["search_description"] # debug fields self.wp_post_id = data["wp_post_id"] self.wp_post_type = data["wp_post_type"] self.wp_link = data["wp_link"] self.wp_raw_content = data["wp_raw_content"] self.wp_block_json = data["wp_block_json"] self.wp_processed_content = data["wp_processed_content"] self.wp_normalized_styles = data["wp_normalized_styles"] self.wp_post_meta = data["wp_post_meta"] # own model fields self.body = data["body"] or "" # categories (organizers and event types) wp_categories = data["wp_categories"] # organizers organizer_cats = [x for x in wp_categories if x["domain"] == "event_organizer"] organizers = [] for x in organizer_cats: try: organizer = EventOrganizer.objects.get(slug=x["nicename"]) except EventOrganizer.DoesNotExist: organizer = EventOrganizer.objects.create(name=x["name"], slug=x["nicename"]) organizers.append(organizer) self.organizer_links.set( [EventOrganizerLink(event=self, organizer=organizer) for organizer in organizers] ) ## event types # type_cats = [x for x in wp_categories if x["domain"] == "event_type"] # event_categories = [] # for x in type_cats: # try: # event_category = EventCategory.objects.get(slug=x["nicename"]) # except EventCategory.DoesNotExist: # event_category = EventCategory.objects.create( # name=x["name"], slug=x["nicename"], show_in_filters=False # ) # event_categories.append(event_category) # self.categories.set(event_categories) meta = data["wp_post_meta"] start_ts = meta.get("neuf_events_starttime") or 1337 end_ts = meta.get("neuf_events_endtime") tz = ZoneInfo("Europe/Oslo") start = start_ts and datetime.datetime.fromtimestamp(start_ts, datetime.UTC).replace( tzinfo=tz ) end = end_ts and datetime.datetime.fromtimestamp(end_ts, datetime.UTC).replace(tzinfo=tz) venue_id = meta.get("neuf_events_venue_id") venue_custom = meta.get("neuf_events_venue") venue = None if venue_id: venue = VenuePage.objects.get(wp_post_id=venue_id) venue_custom = "" else: venue_custom = venue_custom or "" occurrence = EventOccurrence( event=self, start=start, end=end, venue=venue, venue_custom=venue_custom ) self.occurrences.set([occurrence]) self.ticket_url = fix_url(meta.get("neuf_events_bs_url")) or "" self.facebook_url = fix_url(meta.get("neuf_events_fb_url")) or "" def parse_price(price): if price is None: return "" if type(price) is int: return price p = price.strip() if p == "": return "" try: return int(p) except ValueError: pass free = ["gratis", "free", "gratis/free", "free/gratis"] if p.lower() in free: return 0 return price price_regular = parse_price(meta.get("neuf_events_price_regular")) price_member = parse_price(meta.get("neuf_events_price_member")) if not price_regular and not price_member: self.free = True else: self.price_regular = parse_price(meta.get("neuf_events_price_regular")) self.price_member = parse_price(meta.get("neuf_events_price_member")) class EventOccurrence(Orderable): 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( "Bruk denne om ingen av lokalene som kan velges til venstre passer. " "F.eks. Frederikkeplassen eller Sirkusteltet." ), ) panels = [ FieldRowPanel( children=[ FieldPanel("start", heading="Start"), FieldPanel("end", heading="Slutt"), ], ), FieldRowPanel( children=[ FieldPanel("venue", heading="Lokale"), FieldPanel("venue_custom", heading="Lokale som fritekst"), ], ), ] graphql_fields = [ GraphQLString("start", required=True), GraphQLString("end"), GraphQLForeignKey("venue", "venues.VenuePage"), GraphQLString("venue_custom"), ] def clean(self): if self.venue and self.venue_custom: raise ValidationError( {"venue_custom": "Du kan ikke både velge et lokale og skrive noe i dette feltet."} ) if not self.venue and not self.venue_custom: raise ValidationError({"venue": "Lokale er påkrevd."}) def __str__(self): return f"{self.start}--{self.end}" class Meta: verbose_name = "Forekomst" verbose_name_plural = "Forekomster"