a3474fd9cd
* Updates settings and requirements * First pass at attendee profile * Imports the registration templates; defines attendee profile models etc. * First pass at themeing the registration form. * First page of the registration form: done! * Makes form validation nicer * Adds populate_inventory * Improves the additional items page * Allows for rendering of formsets. * Adds support for formset extending. * Removes formset delete buttons * Review page is LCA-ified * Fixes some formset behaviour * Fixes urls.py * LCA-ifies product_category.html * Invoices * Credit card payments * s/register/tickets/ * Show registration features only whilst products are available (think about this better, later) * Updates the attendee profile form page * Form tidy-up * Makes it so that address info is copied from attendee profile to the address details are autofilled in Stripe. * Adds feature to offer Australians a dropdown list of states rather than free text. * Allow toggling of void invoices. * Adds backgrounds to the headers in the registration process * Improves the review page * Adds “Linux Australia” to invoice details. * Do not show balance due on void/refunded invoices. * More thumbing * Adds a link back to reports on each report. * Tokenisation language. * Another bug in credit card processing. * Adds stripe refunds to options * Removes spurious dashboard button. * Tidies up the presentation of discounts. * Tidies up presentation of voucher form. * Fixes sponsor logo appearance with adblock. * Front page tweaks * Lets us specify alternative URLs in homepage panels * more * Updates discount amounts. * More website fixes * Changes language on pay invoice button * Adds contact details to the invoice template. * Updates the currency message in the invoice template. * Explicitly includes e-mail address, because theme_contact_email doesn’t propagate * Changes payment text. * s/registration/selections/ * Removes final face palm * Fixes lack of speaker dinner tickets for actual presenters. * Adjusts wording in invoice e-mails * Invoice wording. * (FIX) * Fixes margins on lists and tables * Improvements arising from those CSS fixes. * Changes description tags.
406 lines
11 KiB
Python
406 lines
11 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models
|
|
from django.db.models.signals import pre_delete
|
|
from django.dispatch import receiver
|
|
from django.forms.utils import ErrorList
|
|
from django.http import Http404
|
|
from django.shortcuts import render
|
|
from django.utils.encoding import python_2_unicode_compatible
|
|
|
|
from modelcluster.fields import ParentalKey
|
|
|
|
from wagtail.wagtailadmin.edit_handlers import InlinePanel
|
|
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
|
from wagtail.wagtailadmin.edit_handlers import PageChooserPanel
|
|
from wagtail.wagtailadmin.edit_handlers import StreamFieldPanel
|
|
|
|
from wagtail.wagtailcore import blocks
|
|
from wagtail.wagtailcore.models import Page
|
|
from wagtail.wagtailcore.models import Orderable
|
|
from wagtail.wagtailcore.fields import RichTextField
|
|
from wagtail.wagtailcore.fields import StreamField
|
|
from wagtail.wagtailcore.url_routing import RouteResult
|
|
|
|
from wagtail.wagtailimages import blocks as imageblocks
|
|
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
|
|
from wagtail.wagtailimages.models import AbstractImage
|
|
from wagtail.wagtailimages.models import AbstractRendition
|
|
from wagtail.wagtailimages.models import Image
|
|
|
|
from wagtail.wagtailsearch import index
|
|
from wagtail.wagtailsnippets.models import register_snippet
|
|
|
|
|
|
from symposion import schedule
|
|
|
|
ILLUSTRATION_ANTARCTICA = "antarctica.svg"
|
|
ILLUSTRATION_BRIDGE = "bridge.svg"
|
|
ILLUSTRATION_CASINO = "casino.svg"
|
|
ILLUSTRATION_CRADLE = "cradle.svg"
|
|
ILLUSTRATION_DEVIL = "devil.svg"
|
|
ILLUSTRATION_FALLS = "falls.svg"
|
|
ILLUSTRATION_HOBART = "hobart.svg"
|
|
ILLUSTRATION_LAVENDER = "lavender.svg"
|
|
ILLUSTRATION_TUZ = "tuz.svg"
|
|
ILLUSTRATION_WINEGLASS = "wineglass.svg"
|
|
|
|
ILLUSTRATION_TYPES = (
|
|
(ILLUSTRATION_ANTARCTICA, "Antarctica"),
|
|
(ILLUSTRATION_BRIDGE, "Bridge"),
|
|
(ILLUSTRATION_CASINO, "Casino"),
|
|
(ILLUSTRATION_CRADLE, "Cradle Mountain"),
|
|
(ILLUSTRATION_DEVIL, "Tasmanian Devil"),
|
|
(ILLUSTRATION_FALLS, "Waterfall"),
|
|
(ILLUSTRATION_HOBART, "Hobart"),
|
|
(ILLUSTRATION_LAVENDER, "Lavender"),
|
|
(ILLUSTRATION_TUZ, "Tuz"),
|
|
(ILLUSTRATION_WINEGLASS, "Wineglass"),
|
|
)
|
|
|
|
|
|
class ExternalLinksBlock(blocks.StructBlock):
|
|
|
|
class Meta:
|
|
template = "cms_pages/home_page_blocks/external_link.html"
|
|
|
|
EXTERNAL_LINK_TWITTER = "twitter"
|
|
EXTERNAL_LINK_FACEBOOK = "facebook"
|
|
EXTERNAL_LINK_GENERIC = "generic"
|
|
|
|
EXTERNAL_LINK_TYPES = (
|
|
(EXTERNAL_LINK_TWITTER, "Twitter"),
|
|
(EXTERNAL_LINK_FACEBOOK, "Facebook"),
|
|
(EXTERNAL_LINK_GENERIC, "Generic URL"),
|
|
)
|
|
|
|
alt = blocks.CharBlock(required=True)
|
|
icon = blocks.ChoiceBlock(
|
|
choices=EXTERNAL_LINK_TYPES,
|
|
required=True,
|
|
)
|
|
url = blocks.URLBlock(required=True)
|
|
|
|
|
|
class BasicContentLink(blocks.StructBlock):
|
|
|
|
page = blocks.PageChooserBlock(
|
|
required=False,
|
|
help_text="You must specify either this, or the URL.",
|
|
)
|
|
url = blocks.CharBlock(
|
|
required=False,
|
|
help_text="You must specify either this, or the URL.",
|
|
)
|
|
title = blocks.CharBlock(required=True)
|
|
|
|
|
|
class BasicContentBlock(blocks.StructBlock):
|
|
|
|
class Meta:
|
|
template = "cms_pages/home_page_blocks/basic_content.html"
|
|
|
|
PANEL_BLUE_LEFT = "blue_left"
|
|
PANEL_WHITE_RIGHT = "white_right"
|
|
PANEL_TYPES = (
|
|
(PANEL_BLUE_LEFT, "Left-aligned image, blue-filtered image BG"),
|
|
(PANEL_WHITE_RIGHT, "Right-aligned image, white background"),
|
|
)
|
|
|
|
panel_type = blocks.ChoiceBlock(
|
|
choices=PANEL_TYPES,
|
|
required=True,
|
|
)
|
|
heading = blocks.CharBlock(required=True)
|
|
inset_illustration = blocks.ChoiceBlock(
|
|
choices=ILLUSTRATION_TYPES,
|
|
required=True,
|
|
)
|
|
background_image = imageblocks.ImageChooserBlock(
|
|
required=False,
|
|
help_text="This is used as the background image of a "
|
|
"blue-left block. It's not used for white-right."
|
|
)
|
|
body = blocks.RichTextBlock(required=True)
|
|
link = BasicContentLink()
|
|
external_links = blocks.ListBlock(ExternalLinksBlock)
|
|
compact = blocks.BooleanBlock(
|
|
required=False,
|
|
help_text="True if this block is to be displayed in 'compact' mode",
|
|
)
|
|
|
|
|
|
class PresentationChooserBlock(blocks.ChooserBlock):
|
|
target_model = schedule.models.Presentation
|
|
widget = forms.Select
|
|
|
|
# Return the key value for the select field
|
|
def value_for_form(self, value):
|
|
if isinstance(value, self.target_model):
|
|
return value.pk
|
|
else:
|
|
return value
|
|
|
|
|
|
class KeynoteSpeakerBlock(blocks.StructBlock):
|
|
|
|
class Meta:
|
|
template = "cms_pages/home_page_blocks/keynote_speaker.html"
|
|
|
|
name = blocks.CharBlock(required=True)
|
|
body = blocks.RichTextBlock(required=True)
|
|
links = blocks.ListBlock(ExternalLinksBlock)
|
|
profile_image = imageblocks.ImageChooserBlock(
|
|
required=False,
|
|
help_text="Profile image for the speaker",
|
|
)
|
|
presentation = PresentationChooserBlock(
|
|
help_text="This speaker's presentation",
|
|
)
|
|
|
|
|
|
class KeynotesBlock(blocks.StructBlock):
|
|
|
|
class Meta:
|
|
template = "cms_pages/home_page_blocks/keynotes.html"
|
|
|
|
heading = blocks.CharBlock(required=True)
|
|
speakers = blocks.ListBlock(KeynoteSpeakerBlock)
|
|
|
|
|
|
class HomePage(Page):
|
|
|
|
body = StreamField([
|
|
("basic_content", BasicContentBlock()),
|
|
("keynotes", KeynotesBlock()),
|
|
# TODO: other bits
|
|
])
|
|
|
|
content_panels = Page.content_panels + [
|
|
StreamFieldPanel('body')
|
|
]
|
|
|
|
|
|
# Content pages
|
|
|
|
class FloatingImageBlock(imageblocks.ImageChooserBlock):
|
|
|
|
class Meta:
|
|
template = "cms_pages/content_page_blocks/floating_image.html"
|
|
|
|
|
|
class AnchorBlock(blocks.CharBlock):
|
|
|
|
class Meta:
|
|
template = "cms_pages/content_page_blocks/anchor.html"
|
|
|
|
|
|
class ColophonImageListBlock(blocks.StructBlock):
|
|
|
|
class Meta:
|
|
template = "cms_pages/content_page_blocks/colophon.html"
|
|
|
|
do_nothing = blocks.BooleanBlock(required=False)
|
|
|
|
|
|
class AbstractContentPage(Page):
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
intro = models.CharField(max_length=250)
|
|
|
|
body = StreamField([
|
|
("rich_text", blocks.RichTextBlock(required=False)),
|
|
("raw_html", blocks.RawHTMLBlock(required=False)),
|
|
("floating_image", FloatingImageBlock()),
|
|
("anchor", AnchorBlock(
|
|
help_text="Add a named anchor to this point in the page"
|
|
)),
|
|
("colophon_image_list", ColophonImageListBlock()),
|
|
])
|
|
|
|
background_image = models.ForeignKey(
|
|
'CustomImage',
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='+'
|
|
)
|
|
|
|
search_fields = Page.search_fields + [
|
|
index.SearchField('intro'),
|
|
index.SearchField('body'),
|
|
]
|
|
|
|
content_panels = Page.content_panels + [
|
|
ImageChooserPanel('background_image'),
|
|
FieldPanel('intro'),
|
|
StreamFieldPanel('body')
|
|
]
|
|
|
|
|
|
class ContentPage(AbstractContentPage):
|
|
|
|
inset_illustration = models.CharField(
|
|
choices=ILLUSTRATION_TYPES,
|
|
max_length=256,
|
|
)
|
|
|
|
content_panels = AbstractContentPage.content_panels + [
|
|
FieldPanel('inset_illustration')
|
|
]
|
|
|
|
|
|
# News pages
|
|
|
|
class NewsIndexPage(AbstractContentPage):
|
|
|
|
def route(self, request, path_components):
|
|
|
|
# Try the default to allow children to resolve
|
|
try:
|
|
return super(NewsIndexPage, self).route(request, path_components)
|
|
except Http404:
|
|
pass
|
|
|
|
if path_components:
|
|
# tell Wagtail to call self.serve() with an additional 'path_components' kwarg
|
|
return RouteResult(self, kwargs={'path_components': path_components})
|
|
else:
|
|
raise Http404
|
|
|
|
def serve(self, request, path_components=[]):
|
|
''' Optionally return the RSS version of the page '''
|
|
|
|
template = self.template
|
|
|
|
if path_components and path_components[0] == "rss":
|
|
template = template.replace(".html", ".rss")
|
|
|
|
r = super(NewsIndexPage, self).serve(request)
|
|
r.template_name = template
|
|
return r
|
|
|
|
def child_pages(self):
|
|
return NewsPage.objects.live().child_of(self).specific().order_by("-date")
|
|
|
|
subpage_types = [
|
|
"NewsPage",
|
|
]
|
|
|
|
content_panels = AbstractContentPage.content_panels
|
|
|
|
|
|
class NewsPage(AbstractContentPage):
|
|
|
|
date = models.DateField("Post date")
|
|
|
|
portrait_image = models.ForeignKey(
|
|
'CustomImage',
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='+'
|
|
)
|
|
|
|
parent_page_types = [
|
|
NewsIndexPage,
|
|
]
|
|
|
|
content_panels = AbstractContentPage.content_panels + [
|
|
FieldPanel('date'),
|
|
ImageChooserPanel('portrait_image'),
|
|
]
|
|
|
|
|
|
@register_snippet
|
|
@python_2_unicode_compatible
|
|
class ScheduleHeaderParagraph(models.Model):
|
|
''' Used to show the paragraph in the header for a schedule page. '''
|
|
schedule = models.OneToOneField(
|
|
schedule.models.Schedule,
|
|
related_name="header_paragraph",
|
|
)
|
|
text = models.TextField()
|
|
|
|
panels = [
|
|
FieldPanel('schedule'),
|
|
FieldPanel('text'),
|
|
]
|
|
|
|
def __str__(self):
|
|
return str(self.schedule)
|
|
|
|
|
|
@register_snippet
|
|
@python_2_unicode_compatible
|
|
class NamedHeaderParagraph(models.Model):
|
|
''' Used to show the paragraph in the header for a schedule page. '''
|
|
name = models.CharField(
|
|
max_length=64,
|
|
help_text="Pass this name to header_paragraph tag.",
|
|
)
|
|
text = models.TextField()
|
|
|
|
panels = [
|
|
FieldPanel('name'),
|
|
FieldPanel('text'),
|
|
]
|
|
|
|
def __str__(self):
|
|
return str(self.name)
|
|
|
|
|
|
# Image models -- copied from wagtail docs
|
|
|
|
|
|
class CustomImage(AbstractImage):
|
|
# Add any extra fields to image here
|
|
|
|
# eg. To add a caption field:
|
|
copyright_year = models.CharField(
|
|
max_length=64,
|
|
help_text="The year the image was taken",
|
|
)
|
|
licence = models.CharField(
|
|
max_length=64,
|
|
help_text="The short-form code for the licence (e.g. CC-BY)",
|
|
)
|
|
author = models.CharField(
|
|
max_length=255,
|
|
help_text="The name of the author of the work",
|
|
)
|
|
source_url = models.URLField(
|
|
help_text="The URL where you can find the original of this image",
|
|
)
|
|
|
|
admin_form_fields = Image.admin_form_fields + (
|
|
"copyright_year",
|
|
"licence",
|
|
"author",
|
|
"source_url",
|
|
)
|
|
|
|
|
|
class CustomRendition(AbstractRendition):
|
|
image = models.ForeignKey(CustomImage, related_name='renditions')
|
|
|
|
class Meta:
|
|
unique_together = (
|
|
('image', 'filter', 'focal_point_key'),
|
|
)
|
|
|
|
|
|
# Delete the source image file when an image is deleted
|
|
@receiver(pre_delete, sender=CustomImage)
|
|
def image_delete(sender, instance, **kwargs):
|
|
instance.file.delete(False)
|
|
|
|
|
|
# Delete the rendition image file when a rendition is deleted
|
|
@receiver(pre_delete, sender=CustomRendition)
|
|
def rendition_delete(sender, instance, **kwargs):
|
|
instance.file.delete(False)
|