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:
James Polley 2017-09-30 01:49:37 +10:00
parent 19e4185cd9
commit 3001324d5e
11 changed files with 773 additions and 69 deletions

View file

@ -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

View 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,)

View file

@ -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):

View file

@ -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

View file

@ -0,0 +1 @@
../../../website/pinaxcon/registrasion/management/commands/generate_badges.py

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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/?$",

View file

@ -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