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

714 lines
24 KiB
Python
Raw Permalink 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, 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_wordpress_import.models import WPImportedPageMixin
from associations.widgets import AssociationChooserWidget
from dnscms.fields import CommonStreamField
from dnscms.options import ALL_PIGS
from venues.models import VenuePage
@register_singular_query_field("eventIndex")
class EventIndex(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):
today = timezone.localtime(timezone.now()).date()
next_occurrence = Min("occurrences__start", filter=Q(occurrences__start__gte=today))
return self.filter(occurrences__start__gte=today).annotate(next_occurrence=next_occurrence)
EventPageManager = PageManager.from_queryset(EventPageQuerySet)
class EventPage(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 <strong>0</strong> 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"),
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")]
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 = ""
settings_panels = Page.settings_panels + WPImportedPageMixin.wordpress_panels
def import_wordpress_data(self, data):
import datetime
import html
from django.core.validators import URLValidator
from zoneinfo import ZoneInfo
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 <em>om ingen av lokalene som kan velges til venstre</em> passer. "
"F.eks. <em>Frederikkeplassen</em> eller <em>Sirkusteltet</em>."
),
)
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"
sample_legacy_event_json = """
{
"id": 64573,
"date": "2023-12-27T11:28:34",
"date_gmt": "2023-12-27T10:28:34",
"guid": {
"rendered": "https://studentersamfundet.no/?post_type=event&#038;p=64573"
},
"modified": "2023-12-27T11:44:11",
"modified_gmt": "2023-12-27T10:44:11",
"slug": "quiz-147-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2",
"status": "publish",
"type": "event",
"link": "https://studentersamfundet.no/arrangement/quiz-147-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-2-3-2-2-2-2-2-2-2-2-2-2-2-2-2/",
"title": {
"rendered": "QUIZ",
"decoded": "QUIZ"
},
"content": {
"rendered": "\n<p>Det Norske Studentersamfund inviterer til quiz hver tirsdag kl. 19:00.</p>\n\n\n\n<p>Vi serverer 50 spørsmål som kan spenne seg fra one hit wonders fra 80-tallet, universets uendelighet, dyrelivets merkverdigheter og mye, mye mer!</p>\n\n\n\n<p>Quiz på Chateau Neuf er åpent for alle. Vinnere og &#8220;lucky losers&#8221; vil bli utnevnt hver kveld. Lag som er over seks personer er tillatt, men da trekkes dere for ett poeng per deltaker per runde.</p>\n\n\n\n<p>For de som ønsker å være med på sammenlagtkonkurransen for høsten vil den regnes ut for de tolv beste prestasjonene laget leverer. Så det vil fremdeles være god sjanse for å vinne sammenlagt selv dere må droppe en quiz eller to for eksamener eller andre forpliktelser.</p>\n\n\n\n<p>Velkommen quizglade mennesker!</p>\n\n\n\n<p>Gratis inngang!</p>\n",
"protected": false
},
"excerpt": {
"rendered": "<p>Det Norske Studentersamfund inviterer til quiz hver tirsdag kl. 19:00. Vi serverer 50 spørsmål som kan spenne seg fra one hit wonders fra 80-tallet, universets uendelighet, dyrelivets merkverdigheter og mye, mye mer! Quiz på Chateau Neuf er åpent for alle. Vinnere og &#8220;lucky losers&#8221; vil bli utnevnt hver kveld. Lag som er over seks personer [&hellip;]</p>\n",
"protected": false
},
"author": 2150,
"featured_media": 64585,
"template": "",
"meta": [],
"event_types": [13],
"event_organizers": [390, 322],
"facebook_url": "https://fb.me/e/2RDR5pZdr",
"ticket_url": "",
"price_regular": "",
"price_member": "",
"start_time": "2024-05-07T17:00:00+00:00",
"end_time": "2024-05-07T20:00:00+00:00",
"venue": "Glassbaren",
"venue_id": "55063",
"thumbnail": {
"thumbnail": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-150x150.png",
"medium": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-300x169.png",
"medium_large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-768x433.png",
"large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1280x720.png",
"1536x1536": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1536x865.png",
"2048x2048": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1.png",
"four-column": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-393x342.png",
"six-column": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-608x342.png",
"extra-large": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1600x901.png",
"newsletter-half": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-320x190.png",
"newsletter-third": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-213x126.png",
"featured": "https://studentersamfundet.no/wp/wp-content/uploads/2023/12/quiz-header-1-1200x480.png"
},
"_links": {
"self": [
{
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573"
}
],
"collection": [
{
"href": "https://studentersamfundet.no/wp-json/wp/v2/events"
}
],
"about": [
{
"href": "https://studentersamfundet.no/wp-json/wp/v2/types/event"
}
],
"author": [
{
"embeddable": true,
"href": "https://studentersamfundet.no/wp-json/wp/v2/users/2150"
}
],
"version-history": [
{
"count": 1,
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573/revisions"
}
],
"predecessor-version": [
{
"id": 64574,
"href": "https://studentersamfundet.no/wp-json/wp/v2/events/64573/revisions/64574"
}
],
"wp:featuredmedia": [
{
"embeddable": true,
"href": "https://studentersamfundet.no/wp-json/wp/v2/media/64585"
}
],
"wp:attachment": [
{
"href": "https://studentersamfundet.no/wp-json/wp/v2/media?parent=64573"
}
],
"wp:term": [
{
"taxonomy": "event_type",
"embeddable": true,
"href": "https://studentersamfundet.no/wp-json/wp/v2/event_types?post=64573"
},
{
"taxonomy": "event_organizer",
"embeddable": true,
"href": "https://studentersamfundet.no/wp-json/wp/v2/event_organizers?post=64573"
}
],
"curies": [
{
"name": "wp",
"href": "https://api.w.org/{rel}",
"templated": true
}
]
}
}
"""