symposion_app/cms_pages/models.py
Christopher Neugebauer a3474fd9cd Adds registration to the website (#69)
* 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.
2016-09-30 20:46:05 +10:00

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)