git subrepo pull vendor/registrasion
subrepo: subdir: "vendor/registrasion" merged: "3545a80" upstream: origin: "git@gitlab.com:tchaypo/registrasion.git" branch: "lca2018" commit: "3545a80" git-subrepo: version: "0.3.1" origin: "???" commit: "???"
This commit is contained in:
parent
19e4185cd9
commit
3001324d5e
11 changed files with 773 additions and 69 deletions
4
vendor/registrasion/.gitrepo
vendored
4
vendor/registrasion/.gitrepo
vendored
|
@ -6,6 +6,6 @@
|
||||||
[subrepo]
|
[subrepo]
|
||||||
remote = git@gitlab.com:tchaypo/registrasion.git
|
remote = git@gitlab.com:tchaypo/registrasion.git
|
||||||
branch = lca2018
|
branch = lca2018
|
||||||
commit = 7cf314adae9f8de1519db63828f55a10aa09f0ee
|
commit = 3545a809e8e14014963c670709b6d0273c0e354a
|
||||||
parent = 7c5c6c02f20ddcbc6c54b3065311880fce586192
|
parent = 19e4185cd9433c8f743c32dde8aee09455db3982
|
||||||
cmdver = 0.3.1
|
cmdver = 0.3.1
|
||||||
|
|
386
vendor/registrasion/registrasion/contrib/badger.py
vendored
Normal file
386
vendor/registrasion/registrasion/contrib/badger.py
vendored
Normal file
|
@ -0,0 +1,386 @@
|
||||||
|
'''
|
||||||
|
Generate Conference Badges
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Nearly all of the code in this was written by Richard Jones for the 2016 conference.
|
||||||
|
That code relied on the user supplying the attendee data in a CSV file, which Richard's
|
||||||
|
code then processed.
|
||||||
|
|
||||||
|
The main (and perhaps only real) difference, here, is that the attendee data are taken
|
||||||
|
directly from the database. No CSV file is required.
|
||||||
|
|
||||||
|
This is now a library with functions / classes referenced by the generate_badges
|
||||||
|
management command, and by the tickets/badger and tickets/badge API functions.
|
||||||
|
'''
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
from lxml import etree
|
||||||
|
import tempfile
|
||||||
|
from copy import deepcopy
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import pdb
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User, Group
|
||||||
|
from pinaxcon.registrasion.models import AttendeeProfile
|
||||||
|
from registrasion.controllers.cart import CartController
|
||||||
|
from registrasion.controllers.invoice import InvoiceController
|
||||||
|
from registrasion.models import Voucher
|
||||||
|
from registrasion.models import Attendee
|
||||||
|
from registrasion.models import Product
|
||||||
|
from registrasion.models import Invoice
|
||||||
|
from symposion.speakers.models import Speaker
|
||||||
|
|
||||||
|
# A few unicode encodings ...
|
||||||
|
GLYPH_PLUS = '+'
|
||||||
|
GLYPH_GLASS = u'\ue001'
|
||||||
|
GLYPH_DINNER = u'\ue179'
|
||||||
|
GLYPH_SPEAKER = u'\ue122'
|
||||||
|
GLYPH_SPRINTS = u'\ue254'
|
||||||
|
GLYPH_CROWN = u'\ue211'
|
||||||
|
GLYPH_SNOWMAN = u'\u2603'
|
||||||
|
GLYPH_STAR = u'\ue007'
|
||||||
|
GLYPH_FLASH = u'\ue162'
|
||||||
|
GLYPH_EDU = u'\ue233'
|
||||||
|
|
||||||
|
# Some company names are too long to fit on the badge, so, we
|
||||||
|
# define abbreviations here.
|
||||||
|
overrides = {
|
||||||
|
"Optiver Pty. Ltd.": "Optiver",
|
||||||
|
"IRESS Market Tech": "IRESS",
|
||||||
|
"The Bureau of Meteorology": "BoM",
|
||||||
|
"Google Australia": "Google",
|
||||||
|
"Facebook Inc.": "Facebook",
|
||||||
|
"Rhapsody Solutions Pty Ltd": "Rhapsody Solutions",
|
||||||
|
"PivotNine Pty Ltd": "PivotNine",
|
||||||
|
"SEEK Ltd.": "SEEK",
|
||||||
|
"UNSW Australia": "UNSW",
|
||||||
|
"Dev Demand Co": "Dev Demand",
|
||||||
|
"Cascode Labs Pty Ltd": "Cascode Labs",
|
||||||
|
"CyberHound Pty Ltd": "CyberHound",
|
||||||
|
"Self employed Contractor": "",
|
||||||
|
"Data Processors Pty Lmt": "Data Processors",
|
||||||
|
"Bureau of Meterology": "BoM",
|
||||||
|
"Google Australia Pty Ltd": "Google",
|
||||||
|
# "NSW Rural Doctors Network": "",
|
||||||
|
"Sense of Security Pty Ltd": "Sense of Security",
|
||||||
|
"Hewlett Packard Enterprose": "HPE",
|
||||||
|
"Hewlett Packard Enterprise": "HPE",
|
||||||
|
"CISCO SYSTEMS INDIA PVT LTD": "CISCO",
|
||||||
|
"The University of Melbourne": "University of Melbourne",
|
||||||
|
"Peter MacCallum Cancer Centre": "Peter Mac",
|
||||||
|
"Commonwealth Bank of Australia": "CBA",
|
||||||
|
"VLSCI, University of Melbourne": "VLSCI",
|
||||||
|
"Australian Bureau of Meteorology": "BoM",
|
||||||
|
"Bureau of Meteorology": "BoM",
|
||||||
|
"Australian Synchrotron | ANSTO": "Australian Synchrotron",
|
||||||
|
"Bureau of Meteorology, Australia": "BoM",
|
||||||
|
"QUT Digital Media Research Centre": "QUT",
|
||||||
|
"Dyn - Dynamic Network Services Inc": "Dyn",
|
||||||
|
"The Australian National University": "ANU",
|
||||||
|
"Murdoch Childrens Research Institute": "MCRI",
|
||||||
|
"Centenary Institute, University of Sydney": "Centenary Institute",
|
||||||
|
"Synchrotron Light Source Australia Pty Ltd": "Australian Synchrotron",
|
||||||
|
"Australian Communication and Media Authority": "ACMA",
|
||||||
|
"Dept. of Education - Camden Haven High School": "Camden Haven High School",
|
||||||
|
"Australian Government - Bureau of Meteorology": "BoM",
|
||||||
|
"The Walter and Eliza Hall Institute of Medical Research": "WEHI",
|
||||||
|
"Dept. Parliamentary Services, Australian Parliamentary Library": "Dept. Parliamentary Services",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def text_size(text, prev=9999):
|
||||||
|
'''
|
||||||
|
Calculate the length of a text string as it relates to font size.
|
||||||
|
'''
|
||||||
|
n = len(text)
|
||||||
|
size = int(min(48, max(28, 28 + 30 * (1 - (n-8) / 11.))))
|
||||||
|
return min(prev, size)
|
||||||
|
|
||||||
|
|
||||||
|
def set_text(soup, text_id, text, resize=None):
|
||||||
|
'''
|
||||||
|
Set the text value of an element (via beautiful soup calls).
|
||||||
|
'''
|
||||||
|
elem = soup.find(".//*[@id='%s']/{http://www.w3.org/2000/svg}tspan" % text_id)
|
||||||
|
if elem is None:
|
||||||
|
raise ValueError('could not find tag id=%s' % text_id)
|
||||||
|
elem.text = text
|
||||||
|
if resize:
|
||||||
|
style = elem.get('style')
|
||||||
|
elem.set('style', style.replace('font-size:60px', 'font-size:%dpx' % resize))
|
||||||
|
|
||||||
|
|
||||||
|
def set_colour(soup, slice_id, colour):
|
||||||
|
'''
|
||||||
|
Set colour of an element (using beautiful soup calls).
|
||||||
|
'''
|
||||||
|
elem = soup.find(".//*[@id='%s']" % slice_id)
|
||||||
|
if elem is None:
|
||||||
|
raise ValueError('could not find tag id=%s' % slice_id)
|
||||||
|
style = elem.get('style')
|
||||||
|
elem.set('style', style.replace('fill:#316a9a', 'fill:#%s' % colour))
|
||||||
|
|
||||||
|
Volunteers = Group.objects.filter(name='Conference volunteers').first().user_set.all()
|
||||||
|
Organisers = Group.objects.filter(name='Conference organisers').first().user_set.all()
|
||||||
|
|
||||||
|
def is_volunteer(attendee):
|
||||||
|
'''
|
||||||
|
Returns True if attendee is in the Conference volunteers group.
|
||||||
|
False otherwise.
|
||||||
|
'''
|
||||||
|
return attendee.user in Volunteers
|
||||||
|
|
||||||
|
def is_organiser(attendee):
|
||||||
|
'''
|
||||||
|
Returns True if attendee is in the Conference volunteers group.
|
||||||
|
False otherwise.
|
||||||
|
'''
|
||||||
|
return attendee.user in Organisers
|
||||||
|
|
||||||
|
|
||||||
|
def svg_badge(soup, data, n):
|
||||||
|
'''
|
||||||
|
Do the actual "heavy lifting" to create the badge SVG
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Python2/3 compat ...
|
||||||
|
try:
|
||||||
|
xx = filter(None, [1, 2, None, 3])[2]
|
||||||
|
filter_None = lambda lst: filter(None, lst)
|
||||||
|
except (TypeError,):
|
||||||
|
filter_None = lambda lst: list(filter(None, lst))
|
||||||
|
|
||||||
|
side = 'lr'[n]
|
||||||
|
for tb in 'tb':
|
||||||
|
part = tb + side
|
||||||
|
lines = [data['firstname'], data['lastname']]
|
||||||
|
if data['promote_company']:
|
||||||
|
lines.append(data['company'])
|
||||||
|
lines.extend([data['line1'], data['line2']])
|
||||||
|
lines = filter_None(lines)[:4]
|
||||||
|
|
||||||
|
lines.extend('' for n in range(4-len(lines)))
|
||||||
|
prev = 9999
|
||||||
|
for m, line in enumerate(lines):
|
||||||
|
size = text_size(line, prev)
|
||||||
|
set_text(soup, 'line-%s-%s' % (part, m), line, size)
|
||||||
|
prev = size
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
if data['organiser']:
|
||||||
|
lines.append('Organiser')
|
||||||
|
set_colour(soup, 'colour-' + part, '319a51')
|
||||||
|
elif data['volunteer']:
|
||||||
|
lines.append('Volunteer')
|
||||||
|
set_colour(soup, 'colour-' + part, '319a51')
|
||||||
|
if data['speaker']:
|
||||||
|
lines.append('Speaker')
|
||||||
|
|
||||||
|
special = bool(lines)
|
||||||
|
|
||||||
|
if 'Friday Only' in data['ticket']:
|
||||||
|
# lines.append('Friday Only')
|
||||||
|
set_colour(soup, 'colour-' + part, 'a83f3f')
|
||||||
|
|
||||||
|
if 'Contributor' in data['ticket']:
|
||||||
|
lines.append('Contributor')
|
||||||
|
elif 'Professional' in data['ticket'] and not data['organiser']:
|
||||||
|
lines.append('Professional')
|
||||||
|
elif 'Sponsor' in data['ticket'] and not data['organiser']:
|
||||||
|
lines.append('Sponsor')
|
||||||
|
elif 'Enthusiast' in data['ticket'] and not data['organiser']:
|
||||||
|
lines.append('Enthusiast')
|
||||||
|
elif data['ticket'] == 'Speaker' and not data['speaker']:
|
||||||
|
lines.append('Speaker')
|
||||||
|
elif not special:
|
||||||
|
if data['ticket']:
|
||||||
|
lines.append(data['ticket'])
|
||||||
|
elif data['friday']:
|
||||||
|
lines.append('Friday Only')
|
||||||
|
set_colour(soup, 'colour-' + part, 'a83f3f')
|
||||||
|
else:
|
||||||
|
lines.append('Tutorial Only')
|
||||||
|
set_colour(soup, 'colour-' + part, 'a83f3f')
|
||||||
|
|
||||||
|
if data['friday'] and data['ticket'] and not data['organiser']:
|
||||||
|
lines.append('Fri, Sat and Sun')
|
||||||
|
if not data['volunteer']:
|
||||||
|
set_colour(soup, 'colour-' + part, '71319a')
|
||||||
|
|
||||||
|
if len(lines) > 3:
|
||||||
|
raise ValueError('lines = %s' % (lines,))
|
||||||
|
|
||||||
|
for n in range(3 - len(lines)):
|
||||||
|
lines.insert(0, '')
|
||||||
|
for m, line in enumerate(lines):
|
||||||
|
size = text_size(line)
|
||||||
|
set_text(soup, 'tags-%s-%s' % (part, m), line, size)
|
||||||
|
|
||||||
|
icons = []
|
||||||
|
if data['sprints']:
|
||||||
|
icons.append(GLYPH_SPRINTS)
|
||||||
|
if data['tutorial']:
|
||||||
|
icons.append(GLYPH_EDU)
|
||||||
|
|
||||||
|
set_text(soup, 'icons-' + part, ' '.join(icons))
|
||||||
|
set_text(soup, 'shirt-' + side, '; '.join(data['shirts']))
|
||||||
|
set_text(soup, 'email-' + side, data['email'])
|
||||||
|
|
||||||
|
|
||||||
|
def collate(options):
|
||||||
|
# If specific usernames were given on the command line, just use those.
|
||||||
|
# Otherwise, use the entire list of attendees.
|
||||||
|
users = User.objects.filter(invoice__status=Invoice.STATUS_PAID)
|
||||||
|
if options['usernames']:
|
||||||
|
users = users.filter(username__in=options['usernames'])
|
||||||
|
|
||||||
|
# Iterate through the attendee list to generate the badges.
|
||||||
|
for n, user in enumerate(users.distinct()):
|
||||||
|
ap = user.attendee.attendeeprofilebase.attendeeprofile
|
||||||
|
data = dict()
|
||||||
|
|
||||||
|
at_nm = ap.name.split()
|
||||||
|
if at_nm[0].lower() in 'mr dr ms mrs miss'.split():
|
||||||
|
at_nm[0] = at_nm[0] + ' ' + at_nm[1]
|
||||||
|
del at_nm[1]
|
||||||
|
if at_nm:
|
||||||
|
data['firstname'] = at_nm[0]
|
||||||
|
data['lastname'] = ' '.join(at_nm[1:])
|
||||||
|
else:
|
||||||
|
print ("ERROR:", ap.attendee.user, 'has no name')
|
||||||
|
continue
|
||||||
|
|
||||||
|
data['line1'] = ap.free_text_1
|
||||||
|
data['line2'] = ap.free_text_2
|
||||||
|
|
||||||
|
data['email'] = user.email
|
||||||
|
data['over18'] = ap.of_legal_age
|
||||||
|
speaker = Speaker.objects.filter(user=user).first()
|
||||||
|
if speaker is None:
|
||||||
|
data['speaker'] = False
|
||||||
|
else:
|
||||||
|
data['speaker'] = bool(speaker.proposals.filter(result__status='accepted'))
|
||||||
|
|
||||||
|
data['paid'] = data['friday'] = data['sprints'] = data['tutorial'] = False
|
||||||
|
data['shirts'] = []
|
||||||
|
data['ticket'] = ''
|
||||||
|
|
||||||
|
# look over all the invoices, yes
|
||||||
|
for inv in Invoice.objects.filter(user_id=ap.attendee.user.id):
|
||||||
|
if not inv.is_paid:
|
||||||
|
continue
|
||||||
|
cart = inv.cart
|
||||||
|
if cart is None:
|
||||||
|
continue
|
||||||
|
data['paid'] = True
|
||||||
|
if cart.productitem_set.filter(product__category__name__startswith="Specialist Day").exists():
|
||||||
|
data['friday'] = True
|
||||||
|
if cart.productitem_set.filter(product__category__name__startswith="Sprint Ticket").exists():
|
||||||
|
data['sprints'] = True
|
||||||
|
if cart.productitem_set.filter(product__category__name__contains="Tutorial").exists():
|
||||||
|
data['tutorial'] = True
|
||||||
|
t = cart.productitem_set.filter(product__category__name__startswith="Conference Ticket")
|
||||||
|
if t.exists():
|
||||||
|
product = t.first().product.name
|
||||||
|
if 'SOLD OUT' not in product:
|
||||||
|
data['ticket'] = product
|
||||||
|
elif cart.productitem_set.filter(product__category__name__contains="Specialist Day Only").exists():
|
||||||
|
data['ticket'] = 'Specialist Day Only'
|
||||||
|
|
||||||
|
data['shirts'].extend(ts.product.name for ts in cart.productitem_set.filter(
|
||||||
|
product__category__name__startswith="T-Shirt"))
|
||||||
|
|
||||||
|
if not data['paid']:
|
||||||
|
print ("INFO:", ap.attendee.user, 'not paid!')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not data['ticket'] and not (data['friday'] or data['tutorial']):
|
||||||
|
print ("ERROR:", ap.attendee.user, 'no conference ticket!')
|
||||||
|
continue
|
||||||
|
|
||||||
|
data['company'] = overrides.get(ap.company, ap.company).strip()
|
||||||
|
|
||||||
|
data['volunteer'] = is_volunteer(ap.attendee)
|
||||||
|
data['organiser'] = is_organiser(ap.attendee)
|
||||||
|
|
||||||
|
if 'Specialist Day Only' in data['ticket']:
|
||||||
|
data['ticket'] = 'Friday Only'
|
||||||
|
|
||||||
|
if 'Conference Organiser' in data['ticket']:
|
||||||
|
data['ticket'] = ''
|
||||||
|
|
||||||
|
if 'Conference Volunteer' in data['ticket']:
|
||||||
|
data['ticket'] = ''
|
||||||
|
|
||||||
|
data['promote_company'] = (
|
||||||
|
data['organiser'] or data['volunteer'] or data['speaker'] or
|
||||||
|
'Sponsor' in data['ticket'] or
|
||||||
|
'Contributor' in data['ticket'] or
|
||||||
|
'Professional' in data['ticket']
|
||||||
|
)
|
||||||
|
|
||||||
|
yield data
|
||||||
|
|
||||||
|
|
||||||
|
def generate_stats(options):
|
||||||
|
stats = {
|
||||||
|
'firstname': [],
|
||||||
|
'lastname': [],
|
||||||
|
'company': [],
|
||||||
|
}
|
||||||
|
for badge in collate(options):
|
||||||
|
stats['firstname'].append((len(badge['firstname']), badge['firstname']))
|
||||||
|
stats['lastname'].append((len(badge['lastname']), badge['lastname']))
|
||||||
|
if badge['promote_company']:
|
||||||
|
stats['company'].append((len(badge['company']), badge['company']))
|
||||||
|
|
||||||
|
stats['firstname'].sort()
|
||||||
|
stats['lastname'].sort()
|
||||||
|
stats['company'].sort()
|
||||||
|
|
||||||
|
for l, s in stats['firstname']:
|
||||||
|
print ('%2d %s' % (l, s))
|
||||||
|
for l, s in stats['lastname']:
|
||||||
|
print ('%2d %s' % (l, s))
|
||||||
|
for l, s in stats['company']:
|
||||||
|
print ('%2d %s' % (l, s))
|
||||||
|
|
||||||
|
|
||||||
|
def generate_badges(options):
|
||||||
|
names = list()
|
||||||
|
|
||||||
|
orig = etree.parse(options['template'])
|
||||||
|
tree = deepcopy(orig)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
for n, data in enumerate(collate(options)):
|
||||||
|
svg_badge(root, data, n % 2)
|
||||||
|
if n % 2:
|
||||||
|
name = os.path.abspath(
|
||||||
|
os.path.join(options['out_dir'], 'badge-%d.svg' % n))
|
||||||
|
tree.write(name)
|
||||||
|
names.append(name)
|
||||||
|
tree = deepcopy(orig)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
if not n % 2:
|
||||||
|
name = os.path.abspath(
|
||||||
|
os.path.join(options['out_dir'], 'badge-%d.svg' % n))
|
||||||
|
tree.write(name)
|
||||||
|
names.append(name)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class InvalidTicketChoiceError(Exception):
|
||||||
|
'''
|
||||||
|
Exception thrown when they chosen ticket isn't valid. This
|
||||||
|
happens either if the ticket choice is 0 (default: Chose a ticket),
|
||||||
|
or is greater than the index if the last ticket choice in the
|
||||||
|
dropdown list.
|
||||||
|
'''
|
||||||
|
def __init__(self, message="Please choose a VALID ticket."):
|
||||||
|
super(InvalidTicketChoiceError, self).__init__(message,)
|
|
@ -9,7 +9,7 @@ import datetime
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
|
@ -64,6 +64,12 @@ class CartController(object):
|
||||||
time_last_updated=timezone.now(),
|
time_last_updated=timezone.now(),
|
||||||
reservation_duration=datetime.timedelta(),
|
reservation_duration=datetime.timedelta(),
|
||||||
)
|
)
|
||||||
|
except MultipleObjectsReturned:
|
||||||
|
# Get the one that looks "newest".
|
||||||
|
existing = commerce.Cart.objects.filter(
|
||||||
|
user=user,
|
||||||
|
status=commerce.Cart.STATUS_ACTIVE,
|
||||||
|
).order_by('-time_last_updated').first()
|
||||||
return cls(existing)
|
return cls(existing)
|
||||||
|
|
||||||
def _fail_if_cart_is_not_active(self):
|
def _fail_if_cart_is_not_active(self):
|
||||||
|
|
57
vendor/registrasion/registrasion/forms.py
vendored
57
vendor/registrasion/registrasion/forms.py
vendored
|
@ -263,7 +263,7 @@ class _CheckboxProductsForm(_ProductsForm):
|
||||||
def set_fields(cls, category, products):
|
def set_fields(cls, category, products):
|
||||||
for product in products:
|
for product in products:
|
||||||
field = forms.BooleanField(
|
field = forms.BooleanField(
|
||||||
label='%s -- %s' % (product.name, product.price),
|
label='%s -- $%s' % (product.name, product.price),
|
||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
cls.base_fields[cls.field_name(product)] = field
|
cls.base_fields[cls.field_name(product)] = field
|
||||||
|
@ -521,3 +521,58 @@ class InvoiceEmailForm(InvoicesWithProductAndStatusForm):
|
||||||
choices=ACTION_CHOICES,
|
choices=ACTION_CHOICES,
|
||||||
initial=ACTION_PREVIEW,
|
initial=ACTION_PREVIEW,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from registrasion.contrib.badger import InvalidTicketChoiceError
|
||||||
|
|
||||||
|
def ticket_selection():
|
||||||
|
return list(enumerate(['!!! NOT A VALID TICKET !!!'] + \
|
||||||
|
[p.name for p in inventory.Product.objects.\
|
||||||
|
filter(category__name__contains="Ticket").\
|
||||||
|
exclude(name__contains="Organiser").order_by('id')]))
|
||||||
|
|
||||||
|
|
||||||
|
class TicketSelectionField(forms.ChoiceField):
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
super(TicketSelectionField, self).validate(value)
|
||||||
|
|
||||||
|
result = int(self.to_python(value))
|
||||||
|
if result <= 0 or result > len(list(self.choices)):
|
||||||
|
raise InvalidTicketChoiceError()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BadgeForm(forms.Form):
|
||||||
|
'''
|
||||||
|
A form for creating one-off badges at rego desk.
|
||||||
|
'''
|
||||||
|
required_css_class = 'label-required'
|
||||||
|
|
||||||
|
name = forms.CharField(label="Name", max_length=60, required=True)
|
||||||
|
email = forms.EmailField(label="Email", max_length=60, required=False)
|
||||||
|
company = forms.CharField(label="Company", max_length=60, required=False)
|
||||||
|
free_text_1 = forms.CharField(label="Free Text", max_length=60, required=False)
|
||||||
|
free_text_2 = forms.CharField(label="Free Text", max_length=60, required=False)
|
||||||
|
|
||||||
|
ticket = TicketSelectionField(label="Select a Ticket", choices=ticket_selection)
|
||||||
|
|
||||||
|
paid = forms.BooleanField(label="Paid", required=False)
|
||||||
|
over18 = forms.BooleanField(label="Over 18", required=False)
|
||||||
|
speaker = forms.BooleanField(label="Speaker", required=False)
|
||||||
|
tutorial = forms.BooleanField(label="Tutorial Ticket", required=False)
|
||||||
|
friday = forms.BooleanField(label="Specialist Day", required=False)
|
||||||
|
sprints = forms.BooleanField(label="Sprints", required=False)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
valid = super(BadgeForm, self).is_valid()
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
return valid
|
||||||
|
|
||||||
|
if self.data['ticket'] == '0': # Invalid ticket type!
|
||||||
|
self.add_error('ticket', 'Please select a VALID ticket type.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
1
vendor/registrasion/registrasion/generate_badges.py
vendored
Symbolic link
1
vendor/registrasion/registrasion/generate_badges.py
vendored
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../../website/pinaxcon/registrasion/management/commands/generate_badges.py
|
|
@ -43,7 +43,7 @@ class Attendee(models.Model):
|
||||||
db_index=True,
|
db_index=True,
|
||||||
)
|
)
|
||||||
completed_registration = models.BooleanField(default=False)
|
completed_registration = models.BooleanField(default=False)
|
||||||
guided_categories_complete = models.ManyToManyField("category")
|
guided_categories_complete = models.ManyToManyField("category", blank=True)
|
||||||
|
|
||||||
|
|
||||||
class AttendeeProfileBase(models.Model):
|
class AttendeeProfileBase(models.Model):
|
||||||
|
|
|
@ -75,6 +75,8 @@ class _ReportTemplateWrapper(object):
|
||||||
def rows(self):
|
def rows(self):
|
||||||
return self.report.rows(self.content_type)
|
return self.report.rows(self.content_type)
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
return self.report.count()
|
||||||
|
|
||||||
class BasicReport(Report):
|
class BasicReport(Report):
|
||||||
|
|
||||||
|
@ -118,6 +120,9 @@ class ListReport(BasicReport):
|
||||||
for i, cell in enumerate(row)
|
for i, cell in enumerate(row)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
return len(self._data)
|
||||||
|
|
||||||
|
|
||||||
class QuerysetReport(BasicReport):
|
class QuerysetReport(BasicReport):
|
||||||
|
|
||||||
|
@ -158,6 +163,9 @@ class QuerysetReport(BasicReport):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
return self._queryset.count()
|
||||||
|
|
||||||
class Links(Report):
|
class Links(Report):
|
||||||
|
|
||||||
def __init__(self, title, links):
|
def __init__(self, title, links):
|
||||||
|
@ -182,6 +190,8 @@ class Links(Report):
|
||||||
self._linked_text(content_type, url, link_text)
|
self._linked_text(content_type, url, link_text)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
return len(self._links)
|
||||||
|
|
||||||
def report_view(title, form_type=None):
|
def report_view(title, form_type=None):
|
||||||
''' Decorator that converts a report view function into something that
|
''' Decorator that converts a report view function into something that
|
||||||
|
|
115
vendor/registrasion/registrasion/reporting/views.py
vendored
115
vendor/registrasion/registrasion/reporting/views.py
vendored
|
@ -13,10 +13,12 @@ from django.db.models import F, Q
|
||||||
from django.db.models import Count, Max, Sum
|
from django.db.models import Count, Max, Sum
|
||||||
from django.db.models import Case, When, Value
|
from django.db.models import Case, When, Value
|
||||||
from django.db.models.fields.related import RelatedField
|
from django.db.models.fields.related import RelatedField
|
||||||
|
from django.db.models.fields import CharField
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from registrasion.controllers.cart import CartController
|
from registrasion.controllers.cart import CartController
|
||||||
from registrasion.controllers.item import ItemController
|
from registrasion.controllers.item import ItemController
|
||||||
|
from registrasion.models import conditions
|
||||||
from registrasion.models import commerce
|
from registrasion.models import commerce
|
||||||
from registrasion.models import people
|
from registrasion.models import people
|
||||||
from registrasion import util
|
from registrasion import util
|
||||||
|
@ -30,6 +32,8 @@ from .reports import ListReport
|
||||||
from .reports import QuerysetReport
|
from .reports import QuerysetReport
|
||||||
from .reports import report_view
|
from .reports import report_view
|
||||||
|
|
||||||
|
import bleach
|
||||||
|
|
||||||
|
|
||||||
def CURRENCY():
|
def CURRENCY():
|
||||||
return models.DecimalField(decimal_places=2)
|
return models.DecimalField(decimal_places=2)
|
||||||
|
@ -240,6 +244,83 @@ def group_by_cart_status(queryset, order, values):
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
@report_view("Limits")
|
||||||
|
def limits(request, form):
|
||||||
|
''' Shows the summary of sales against stock limits. '''
|
||||||
|
|
||||||
|
line_items = commerce.ProductItem.objects.filter(
|
||||||
|
cart__status=commerce.Invoice.STATUS_PAID,
|
||||||
|
).values(
|
||||||
|
"product", "product__name",
|
||||||
|
).annotate(
|
||||||
|
total_quantity=Sum("quantity")
|
||||||
|
)
|
||||||
|
|
||||||
|
quantities = collections.defaultdict(int)
|
||||||
|
for line_item in line_items.all():
|
||||||
|
quantities[line_item['product__name']] += line_item['total_quantity']
|
||||||
|
|
||||||
|
limits = conditions.TimeOrStockLimitFlag.objects.all().order_by("-limit")
|
||||||
|
|
||||||
|
headings = ["Product", "Quantity"]
|
||||||
|
|
||||||
|
reports = []
|
||||||
|
for limit in limits:
|
||||||
|
data = []
|
||||||
|
total = 0
|
||||||
|
for product in limit.products.all():
|
||||||
|
if product.name in quantities:
|
||||||
|
total += quantities[product.name]
|
||||||
|
data.append([product.name, quantities[product.name]])
|
||||||
|
if limit.limit:
|
||||||
|
data.append(['(TOTAL)', '%s/%s' % (total, limit.limit)])
|
||||||
|
else:
|
||||||
|
data.append(['(TOTAL)', total])
|
||||||
|
|
||||||
|
description = limit.description
|
||||||
|
extras = []
|
||||||
|
if limit.start_time:
|
||||||
|
extras.append('Starts: %s' % (limit.start_time))
|
||||||
|
|
||||||
|
if limit.end_time:
|
||||||
|
extras.append('Ends: %s' % (limit.end_time))
|
||||||
|
|
||||||
|
if extras:
|
||||||
|
description += ' (' + ', '.join(extras) + ')'
|
||||||
|
|
||||||
|
reports.append(ListReport(description, headings, data))
|
||||||
|
|
||||||
|
# now get discount items
|
||||||
|
discount_items = conditions.DiscountBase.objects.select_subclasses()
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for discount in discount_items.all():
|
||||||
|
quantity = 0
|
||||||
|
for item in discount.discountitem_set.filter(cart__status=2):
|
||||||
|
quantity += item.quantity
|
||||||
|
|
||||||
|
description = discount.description
|
||||||
|
extras = []
|
||||||
|
if getattr(discount, 'start_time', None):
|
||||||
|
extras.append('Starts: %s' % (discount.start_time))
|
||||||
|
|
||||||
|
if getattr(discount, 'end_time', None):
|
||||||
|
extras.append('Ends: %s' % (discount.end_time))
|
||||||
|
|
||||||
|
if extras:
|
||||||
|
description += ' (' + ', '.join(extras) + ')'
|
||||||
|
|
||||||
|
if getattr(discount, 'limit', None):
|
||||||
|
data.append([description, '%s/%s' % (quantity, discount.limit)])
|
||||||
|
else:
|
||||||
|
data.append([description, quantity])
|
||||||
|
|
||||||
|
headings = ["Discount", "Quantity"]
|
||||||
|
reports.append(ListReport('Discounts', headings, data))
|
||||||
|
|
||||||
|
return reports
|
||||||
|
|
||||||
|
|
||||||
@report_view("Product status", form_type=forms.ProductAndCategoryForm)
|
@report_view("Product status", form_type=forms.ProductAndCategoryForm)
|
||||||
def product_status(request, form):
|
def product_status(request, form):
|
||||||
''' Summarises the inventory status of the given items, grouping by
|
''' Summarises the inventory status of the given items, grouping by
|
||||||
|
@ -329,7 +410,7 @@ def product_line_items(request, form):
|
||||||
"user",
|
"user",
|
||||||
"user__attendee",
|
"user__attendee",
|
||||||
"user__attendee__attendeeprofilebase"
|
"user__attendee__attendeeprofilebase"
|
||||||
).order_by("issue_time")
|
).order_by("issue_time").distinct()
|
||||||
|
|
||||||
headings = [
|
headings = [
|
||||||
'Invoice', 'Invoice Date', 'Attendee', 'Qty', 'Product', 'Status'
|
'Invoice', 'Invoice Date', 'Attendee', 'Qty', 'Product', 'Status'
|
||||||
|
@ -386,7 +467,7 @@ def paid_invoices_by_date(request, form):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Zero-value invoices will have no payments, so they're paid at issue time
|
# Zero-value invoices will have no payments, so they're paid at issue time
|
||||||
zero_value_invoices = invoices.filter(value=0)
|
zero_value_invoices = invoices.filter(value=0).distinct()
|
||||||
|
|
||||||
times = itertools.chain(
|
times = itertools.chain(
|
||||||
(line["max_time"] for line in invoice_max_time),
|
(line["max_time"] for line in invoice_max_time),
|
||||||
|
@ -464,18 +545,19 @@ def attendee(request, form, user_id=None):
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
return attendee_list(request)
|
return attendee_list(request)
|
||||||
|
|
||||||
attendee = people.Attendee.objects.get(user__id=user_id)
|
|
||||||
name = attendee.attendeeprofilebase.attendee_name()
|
|
||||||
|
|
||||||
reports = []
|
reports = []
|
||||||
|
|
||||||
profile_data = []
|
profile_data = []
|
||||||
try:
|
try:
|
||||||
|
attendee = people.Attendee.objects.get(user__id=user_id)
|
||||||
|
name = attendee.attendeeprofilebase.attendee_name()
|
||||||
|
|
||||||
profile = people.AttendeeProfileBase.objects.get_subclass(
|
profile = people.AttendeeProfileBase.objects.get_subclass(
|
||||||
attendee=attendee
|
attendee=attendee
|
||||||
)
|
)
|
||||||
fields = profile._meta.get_fields()
|
fields = profile._meta.get_fields()
|
||||||
except people.AttendeeProfileBase.DoesNotExist:
|
except people.AttendeeProfileBase.DoesNotExist:
|
||||||
|
name = attendee.user.username
|
||||||
fields = []
|
fields = []
|
||||||
|
|
||||||
exclude = set(["attendeeprofilebase_ptr", "id"])
|
exclude = set(["attendeeprofilebase_ptr", "id"])
|
||||||
|
@ -489,11 +571,17 @@ def attendee(request, form, user_id=None):
|
||||||
|
|
||||||
if isinstance(field, models.ManyToManyField):
|
if isinstance(field, models.ManyToManyField):
|
||||||
value = ", ".join(str(i) for i in value.all())
|
value = ", ".join(str(i) for i in value.all())
|
||||||
|
elif isinstance(field, CharField):
|
||||||
|
value = bleach.clean(value)
|
||||||
|
|
||||||
profile_data.append((field.verbose_name, value))
|
profile_data.append((field.verbose_name, value))
|
||||||
|
|
||||||
cart = CartController.for_user(attendee.user)
|
cart = CartController.for_user(attendee.user)
|
||||||
|
try:
|
||||||
reservation = cart.cart.reservation_duration + cart.cart.time_last_updated
|
reservation = cart.cart.reservation_duration + cart.cart.time_last_updated
|
||||||
|
except AttributeError: # No reservation_duration set -- default to 24h
|
||||||
|
reservation = datetime.datetime.now() + datetime.timedelta(hours=24)
|
||||||
|
|
||||||
profile_data.append(("Current cart reserved until", reservation))
|
profile_data.append(("Current cart reserved until", reservation))
|
||||||
|
|
||||||
reports.append(ListReport("Profile", ["", ""], profile_data))
|
reports.append(ListReport("Profile", ["", ""], profile_data))
|
||||||
|
@ -515,6 +603,7 @@ def attendee(request, form, user_id=None):
|
||||||
reports.append(Links("Actions for " + name, links))
|
reports.append(Links("Actions for " + name, links))
|
||||||
|
|
||||||
# Paid and pending products
|
# Paid and pending products
|
||||||
|
try:
|
||||||
ic = ItemController(attendee.user)
|
ic = ItemController(attendee.user)
|
||||||
reports.append(ListReport(
|
reports.append(ListReport(
|
||||||
"Paid Products",
|
"Paid Products",
|
||||||
|
@ -526,8 +615,11 @@ def attendee(request, form, user_id=None):
|
||||||
["Product", "Quantity"],
|
["Product", "Quantity"],
|
||||||
[(pq.product, pq.quantity) for pq in ic.items_pending()],
|
[(pq.product, pq.quantity) for pq in ic.items_pending()],
|
||||||
))
|
))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Invoices
|
# Invoices
|
||||||
|
try:
|
||||||
invoices = commerce.Invoice.objects.filter(
|
invoices = commerce.Invoice.objects.filter(
|
||||||
user=attendee.user,
|
user=attendee.user,
|
||||||
)
|
)
|
||||||
|
@ -538,8 +630,11 @@ def attendee(request, form, user_id=None):
|
||||||
headings=["Invoice ID", "Status", "Value"],
|
headings=["Invoice ID", "Status", "Value"],
|
||||||
link_view=views.invoice,
|
link_view=views.invoice,
|
||||||
))
|
))
|
||||||
|
except AttrbuteError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Credit Notes
|
# Credit Notes
|
||||||
|
try:
|
||||||
credit_notes = commerce.CreditNote.objects.filter(
|
credit_notes = commerce.CreditNote.objects.filter(
|
||||||
invoice__user=attendee.user,
|
invoice__user=attendee.user,
|
||||||
).select_related("invoice", "creditnoteapplication", "creditnoterefund")
|
).select_related("invoice", "creditnoteapplication", "creditnoterefund")
|
||||||
|
@ -550,8 +645,11 @@ def attendee(request, form, user_id=None):
|
||||||
credit_notes,
|
credit_notes,
|
||||||
link_view=views.credit_note,
|
link_view=views.credit_note,
|
||||||
))
|
))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
# All payments
|
# All payments
|
||||||
|
try:
|
||||||
payments = commerce.PaymentBase.objects.filter(
|
payments = commerce.PaymentBase.objects.filter(
|
||||||
invoice__user=attendee.user,
|
invoice__user=attendee.user,
|
||||||
).select_related("invoice")
|
).select_related("invoice")
|
||||||
|
@ -562,6 +660,8 @@ def attendee(request, form, user_id=None):
|
||||||
payments,
|
payments,
|
||||||
link_view=views.invoice,
|
link_view=views.invoice,
|
||||||
))
|
))
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
return reports
|
return reports
|
||||||
|
|
||||||
|
@ -678,6 +778,7 @@ def attendee_data(request, form, user_id=None):
|
||||||
category = product + "__category"
|
category = product + "__category"
|
||||||
category_name = category + "__name"
|
category_name = category + "__name"
|
||||||
|
|
||||||
|
|
||||||
if by_category:
|
if by_category:
|
||||||
grouping_fields = (category, category_name)
|
grouping_fields = (category, category_name)
|
||||||
order_by = (category, )
|
order_by = (category, )
|
||||||
|
@ -712,7 +813,7 @@ def attendee_data(request, form, user_id=None):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
def display_field(value):
|
def display_field(value):
|
||||||
return value
|
return bleach.clean(value)
|
||||||
|
|
||||||
status_count = lambda status: Case(When( # noqa
|
status_count = lambda status: Case(When( # noqa
|
||||||
attendee__user__cart__status=status,
|
attendee__user__cart__status=status,
|
||||||
|
@ -759,7 +860,7 @@ def attendee_data(request, form, user_id=None):
|
||||||
if isinstance(field_type, models.ManyToManyField):
|
if isinstance(field_type, models.ManyToManyField):
|
||||||
return [str(i) for i in attr.all()] or ""
|
return [str(i) for i in attr.all()] or ""
|
||||||
else:
|
else:
|
||||||
return attr
|
return bleach.clean(attr)
|
||||||
|
|
||||||
headings = ["User ID", "Name", "Email", "Product", "Item Status"]
|
headings = ["User ID", "Name", "Email", "Product", "Item Status"]
|
||||||
headings.extend(field_names)
|
headings.extend(field_names)
|
||||||
|
|
|
@ -10,6 +10,8 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,8 +48,9 @@ def missing_categories(context):
|
||||||
for product, quantity in items:
|
for product, quantity in items:
|
||||||
categories_held.add(product.category)
|
categories_held.add(product.category)
|
||||||
|
|
||||||
return categories_available - categories_held
|
missing = categories_available - categories_held
|
||||||
|
|
||||||
|
return sorted(set(i for i in missing), key=attrgetter("order"))
|
||||||
|
|
||||||
@register.assignment_tag(takes_context=True)
|
@register.assignment_tag(takes_context=True)
|
||||||
def available_credit(context):
|
def available_credit(context):
|
||||||
|
|
8
vendor/registrasion/registrasion/urls.py
vendored
8
vendor/registrasion/registrasion/urls.py
vendored
|
@ -6,7 +6,7 @@ from django.conf.urls import url
|
||||||
from .views import (
|
from .views import (
|
||||||
amend_registration,
|
amend_registration,
|
||||||
badge,
|
badge,
|
||||||
badges,
|
badger,
|
||||||
checkout,
|
checkout,
|
||||||
credit_note,
|
credit_note,
|
||||||
edit_profile,
|
edit_profile,
|
||||||
|
@ -26,7 +26,8 @@ from .views import (
|
||||||
public = [
|
public = [
|
||||||
url(r"^amend/([0-9]+)$", amend_registration, name="amend_registration"),
|
url(r"^amend/([0-9]+)$", amend_registration, name="amend_registration"),
|
||||||
url(r"^badge/([0-9]+)$", badge, name="badge"),
|
url(r"^badge/([0-9]+)$", badge, name="badge"),
|
||||||
url(r"^badges$", badges, name="badges"),
|
url(r"^badger/([A-Za-z0-9]+)$", badger, name="badger"),
|
||||||
|
url(r"^badger/", badger, name="badger"),
|
||||||
url(r"^category/([0-9]+)$", product_category, name="product_category"),
|
url(r"^category/([0-9]+)$", product_category, name="product_category"),
|
||||||
url(r"^checkout$", checkout, name="checkout"),
|
url(r"^checkout$", checkout, name="checkout"),
|
||||||
url(r"^checkout/([0-9]+)$", checkout, name="checkout"),
|
url(r"^checkout/([0-9]+)$", checkout, name="checkout"),
|
||||||
|
@ -42,9 +43,9 @@ public = [
|
||||||
name="invoice_access"),
|
name="invoice_access"),
|
||||||
url(r"^invoice_mailout$", invoice_mailout, name="invoice_mailout"),
|
url(r"^invoice_mailout$", invoice_mailout, name="invoice_mailout"),
|
||||||
url(r"^profile$", edit_profile, name="attendee_edit"),
|
url(r"^profile$", edit_profile, name="attendee_edit"),
|
||||||
url(r"^register$", guided_registration, name="guided_registration"),
|
|
||||||
url(r"^review$", review, name="review"),
|
url(r"^review$", review, name="review"),
|
||||||
url(r"^voucher$", voucher_code, name="voucher_code"),
|
url(r"^voucher$", voucher_code, name="voucher_code"),
|
||||||
|
url(r"^register$", guided_registration, name="guided_registration"),
|
||||||
url(r"^register/([0-9]+)$", guided_registration,
|
url(r"^register/([0-9]+)$", guided_registration,
|
||||||
name="guided_registration"),
|
name="guided_registration"),
|
||||||
]
|
]
|
||||||
|
@ -56,6 +57,7 @@ reports = [
|
||||||
url(r"^attendee_data/?$", rv.attendee_data, name="attendee_data"),
|
url(r"^attendee_data/?$", rv.attendee_data, name="attendee_data"),
|
||||||
url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"),
|
url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"),
|
||||||
url(r"^credit_notes/?$", rv.credit_notes, name="credit_notes"),
|
url(r"^credit_notes/?$", rv.credit_notes, name="credit_notes"),
|
||||||
|
url(r"^limits/?$", rv.limits, name="reconciliation"),
|
||||||
url(r"^manifest/?$", rv.manifest, name="manifest"),
|
url(r"^manifest/?$", rv.manifest, name="manifest"),
|
||||||
url(
|
url(
|
||||||
r"^product_line_items/?$",
|
r"^product_line_items/?$",
|
||||||
|
|
166
vendor/registrasion/registrasion/views.py
vendored
166
vendor/registrasion/registrasion/views.py
vendored
|
@ -1,5 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import os
|
||||||
|
|
||||||
from . import forms
|
from . import forms
|
||||||
from . import util
|
from . import util
|
||||||
|
@ -32,6 +33,16 @@ from django.shortcuts import redirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.template import Context, Template, loader
|
from django.template import Context, Template, loader
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from registrasion.forms import BadgeForm, ticket_selection
|
||||||
|
from registrasion.contrib.badger import (
|
||||||
|
collate,
|
||||||
|
svg_badge,
|
||||||
|
InvalidTicketChoiceError
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_GuidedRegistrationSection = namedtuple(
|
_GuidedRegistrationSection = namedtuple(
|
||||||
"GuidedRegistrationSection",
|
"GuidedRegistrationSection",
|
||||||
|
@ -544,11 +555,13 @@ def _handle_products(request, category, products, prefix):
|
||||||
# If category is required, the user must have at least one
|
# If category is required, the user must have at least one
|
||||||
# in an active+valid cart
|
# in an active+valid cart
|
||||||
if category.required:
|
if category.required:
|
||||||
carts = commerce.Cart.objects.filter(user=request.user)
|
carts = commerce.Cart.objects.filter(user=request.user,
|
||||||
|
status=commerce.Cart.STATUS_ACTIVE)
|
||||||
items = commerce.ProductItem.objects.filter(
|
items = commerce.ProductItem.objects.filter(
|
||||||
product__category=category,
|
product__category=category,
|
||||||
cart=carts,
|
cart=current_cart.cart,
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(items) == 0:
|
if len(items) == 0:
|
||||||
products_form.add_error(
|
products_form.add_error(
|
||||||
None,
|
None,
|
||||||
|
@ -1076,24 +1089,42 @@ def invoice_mailout(request):
|
||||||
|
|
||||||
return render(request, "registrasion/invoice_mailout.html", data)
|
return render(request, "registrasion/invoice_mailout.html", data)
|
||||||
|
|
||||||
|
def _get_badge_template_name():
|
||||||
|
return os.path.join(settings.PROJECT_ROOT, 'pinaxcon', 'templates',
|
||||||
|
settings.BADGER_DEFAULT_SVG)
|
||||||
|
|
||||||
@user_passes_test(_staff_only)
|
@user_passes_test(_staff_only)
|
||||||
def badge(request, user_id):
|
def badge(request, user_id):
|
||||||
''' Renders a single user's badge (SVG). '''
|
'''
|
||||||
|
Renders a single user's badge (SVG).
|
||||||
|
|
||||||
|
This does little more than call Richard Jones' collate and svg_badge
|
||||||
|
functions found in generate_badges.
|
||||||
|
'''
|
||||||
|
|
||||||
user_id = int(user_id)
|
user_id = int(user_id)
|
||||||
user = User.objects.get(pk=user_id)
|
user = User.objects.get(pk=user_id)
|
||||||
|
|
||||||
rendered = render_badge(user)
|
# This will fail spectacularly -- will put exception handling in later ...
|
||||||
response = HttpResponse(rendered)
|
user_data = list(collate({'usernames': [user.username]}))[0]
|
||||||
|
|
||||||
|
orig = etree.parse(_get_badge_template_name())
|
||||||
|
tree = deepcopy(orig)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
svg_badge(root, user_data, 0)
|
||||||
|
|
||||||
|
response = HttpResponse(etree.tostring(root))
|
||||||
response["Content-Type"] = "image/svg+xml"
|
response["Content-Type"] = "image/svg+xml"
|
||||||
response["Content-Disposition"] = 'inline; filename="badge.svg"'
|
response["Content-Disposition"] = 'inline; filename="badge.svg"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def badges(request):
|
def badges(request):
|
||||||
''' Either displays a form containing a list of users with badges to
|
'''
|
||||||
|
*** NOT USED FOR PYCONAU 2017 MELBOURNE (I.e., not supported in badger module.) ***
|
||||||
|
|
||||||
|
Either displays a form containing a list of users with badges to
|
||||||
render, or returns a .zip file containing their badges. '''
|
render, or returns a .zip file containing their badges. '''
|
||||||
|
|
||||||
category = request.GET.getlist("category", [])
|
category = request.GET.getlist("category", [])
|
||||||
|
@ -1128,12 +1159,121 @@ def badges(request):
|
||||||
return render(request, "registrasion/badges.html", data)
|
return render(request, "registrasion/badges.html", data)
|
||||||
|
|
||||||
|
|
||||||
def render_badge(user):
|
def collate_from_form(form):
|
||||||
''' Renders a single user's badge. '''
|
'''
|
||||||
|
Does what collate does, but using form data as its input source
|
||||||
|
rather than User record.
|
||||||
|
'''
|
||||||
|
# Build the thing we'll pass to svg_badge() later.
|
||||||
|
data = dict()
|
||||||
|
|
||||||
data = {
|
# Get the name bits ...
|
||||||
"user": user,
|
at_nm = form.data['name'].split()
|
||||||
}
|
if at_nm[0].lower() in 'mr dr ms mrs miss'.split():
|
||||||
|
at_nm[0] = at_nm[0] + ' ' + at_nm[1]
|
||||||
|
del at_nm[1]
|
||||||
|
if at_nm:
|
||||||
|
data['firstname'] = at_nm[0]
|
||||||
|
data['lastname'] = ''.join(at_nm[1:])
|
||||||
|
else: # Can't happen -- form validator will check for this.
|
||||||
|
pass
|
||||||
|
|
||||||
t = loader.get_template('registrasion/badge.svg')
|
# Free text -- only one line ... come on!
|
||||||
return t.render(data)
|
data['line1'] = form.data['free_text_1']
|
||||||
|
data['line2'] = form.data['free_text_2']
|
||||||
|
|
||||||
|
# Email ...
|
||||||
|
data['email'] = form.data['email']
|
||||||
|
|
||||||
|
# Don't think we want to allow ad hoc organiser tickets ...
|
||||||
|
data['organiser'] = False
|
||||||
|
|
||||||
|
# Punt on shirts for now ...
|
||||||
|
data['shirts'] = list()
|
||||||
|
|
||||||
|
# Lots booleans ...
|
||||||
|
for key in ['over18', 'paid', 'friday', 'speaker', 'tutorial',
|
||||||
|
'sprints', 'company',]:
|
||||||
|
data[key] = form.data.get(key, False)
|
||||||
|
|
||||||
|
# This will throw InvalidTicketChoiceError if the ticket
|
||||||
|
# choice isn't found in the ticket list or is the
|
||||||
|
# "Plese select a valid tickt" choice. (I.e., they forgot
|
||||||
|
# to choose a ticket.)
|
||||||
|
data['ticket'] = ticket_selection()[int(form.data['ticket'])][1]
|
||||||
|
|
||||||
|
data['volunteer'] = data['ticket'].find("Volunteer") >= 0
|
||||||
|
|
||||||
|
if 'Specialist Day Only' in data['ticket']:
|
||||||
|
data['ticket'] = 'Friday Only'
|
||||||
|
data['friday'] = True
|
||||||
|
|
||||||
|
if 'Conference Organiser' in data['ticket']:
|
||||||
|
data['ticket'] = ''
|
||||||
|
|
||||||
|
if 'Conference Volunteer' in data['ticket']:
|
||||||
|
data['ticket'] = ''
|
||||||
|
|
||||||
|
data['promote_company'] = (
|
||||||
|
data['organiser'] or data['volunteer'] or data['speaker'] or
|
||||||
|
'Sponsor' in data['ticket'] or
|
||||||
|
'Contributor' in data['ticket'] or
|
||||||
|
'Professional' in data['ticket']
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@user_passes_test(_staff_only)
|
||||||
|
def badger(request, username=None):
|
||||||
|
'''
|
||||||
|
Renders a single user's badge from data supplied on
|
||||||
|
a form rather than from Attendee data.
|
||||||
|
|
||||||
|
If *username* is provided in the URL, an attempt
|
||||||
|
will be made to look up this user and fill in the
|
||||||
|
badge details from the User and Attendee records.
|
||||||
|
'''
|
||||||
|
|
||||||
|
if username is not None:
|
||||||
|
|
||||||
|
# We have a username. Try to populate our badge data
|
||||||
|
# from User/Attendee model.
|
||||||
|
try:
|
||||||
|
data = collate({'usernames': [username,]}).next()
|
||||||
|
except: # No matching User record (probably) ... just put up a blank form
|
||||||
|
return render(request, settings.BADGER_DEFAULT_FORM, {'form': BadgeForm})
|
||||||
|
else:
|
||||||
|
form = BadgeForm(request.POST)
|
||||||
|
|
||||||
|
if len(form.data) == 0: # Empty or request to put up the form.
|
||||||
|
return render(request, settings.BADGER_DEFAULT_FORM, {'form': BadgeForm})
|
||||||
|
|
||||||
|
try:
|
||||||
|
if form.is_valid():
|
||||||
|
data = collate_from_form(form)
|
||||||
|
|
||||||
|
except InvalidTicketChoiceError:
|
||||||
|
form.add_error('ticket', 'Please select a VALID ticket type!')
|
||||||
|
return render(request, settings.BADGER_DEFAULT_FORM, {'form': form})
|
||||||
|
|
||||||
|
except TypeError as e:
|
||||||
|
form.add_error(e.message)
|
||||||
|
return render(request, settings.BADGER_DEFAULT_FORM, {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
# We should have valid data if we get this far.
|
||||||
|
# Fill in the template and return the resulting SVG object.
|
||||||
|
orig = etree.parse(_get_badge_template_name())
|
||||||
|
tree = deepcopy(orig)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Generate the badge (svg)
|
||||||
|
svg_badge(root, data, 0)
|
||||||
|
|
||||||
|
# Ship it back to the user...
|
||||||
|
response = HttpResponse(etree.tostring(root))
|
||||||
|
|
||||||
|
response["Content-Type"] = "image/svg+xml"
|
||||||
|
response["Content-Disposition"] = 'inline; filename="badge.svg"'
|
||||||
|
return response
|
||||||
|
|
Loading…
Reference in a new issue