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]
|
||||
remote = git@gitlab.com:tchaypo/registrasion.git
|
||||
branch = lca2018
|
||||
commit = 7cf314adae9f8de1519db63828f55a10aa09f0ee
|
||||
parent = 7c5c6c02f20ddcbc6c54b3065311880fce586192
|
||||
commit = 3545a809e8e14014963c670709b6d0273c0e354a
|
||||
parent = 19e4185cd9433c8f743c32dde8aee09455db3982
|
||||
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 itertools
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import Max
|
||||
|
@ -64,6 +64,12 @@ class CartController(object):
|
|||
time_last_updated=timezone.now(),
|
||||
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)
|
||||
|
||||
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):
|
||||
for product in products:
|
||||
field = forms.BooleanField(
|
||||
label='%s -- %s' % (product.name, product.price),
|
||||
label='%s -- $%s' % (product.name, product.price),
|
||||
required=False,
|
||||
)
|
||||
cls.base_fields[cls.field_name(product)] = field
|
||||
|
@ -521,3 +521,58 @@ class InvoiceEmailForm(InvoicesWithProductAndStatusForm):
|
|||
choices=ACTION_CHOICES,
|
||||
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,
|
||||
)
|
||||
completed_registration = models.BooleanField(default=False)
|
||||
guided_categories_complete = models.ManyToManyField("category")
|
||||
guided_categories_complete = models.ManyToManyField("category", blank=True)
|
||||
|
||||
|
||||
class AttendeeProfileBase(models.Model):
|
||||
|
|
|
@ -75,6 +75,8 @@ class _ReportTemplateWrapper(object):
|
|||
def rows(self):
|
||||
return self.report.rows(self.content_type)
|
||||
|
||||
def count(self):
|
||||
return self.report.count()
|
||||
|
||||
class BasicReport(Report):
|
||||
|
||||
|
@ -118,6 +120,9 @@ class ListReport(BasicReport):
|
|||
for i, cell in enumerate(row)
|
||||
]
|
||||
|
||||
def count(self):
|
||||
return len(self._data)
|
||||
|
||||
|
||||
class QuerysetReport(BasicReport):
|
||||
|
||||
|
@ -158,6 +163,9 @@ class QuerysetReport(BasicReport):
|
|||
]
|
||||
|
||||
|
||||
def count(self):
|
||||
return self._queryset.count()
|
||||
|
||||
class Links(Report):
|
||||
|
||||
def __init__(self, title, links):
|
||||
|
@ -182,6 +190,8 @@ class Links(Report):
|
|||
self._linked_text(content_type, url, link_text)
|
||||
]
|
||||
|
||||
def count(self):
|
||||
return len(self._links)
|
||||
|
||||
def report_view(title, form_type=None):
|
||||
''' Decorator that converts a report view function into something that
|
||||
|
|
195
vendor/registrasion/registrasion/reporting/views.py
vendored
195
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 Case, When, Value
|
||||
from django.db.models.fields.related import RelatedField
|
||||
from django.db.models.fields import CharField
|
||||
from django.shortcuts import render
|
||||
|
||||
from registrasion.controllers.cart import CartController
|
||||
from registrasion.controllers.item import ItemController
|
||||
from registrasion.models import conditions
|
||||
from registrasion.models import commerce
|
||||
from registrasion.models import people
|
||||
from registrasion import util
|
||||
|
@ -30,6 +32,8 @@ from .reports import ListReport
|
|||
from .reports import QuerysetReport
|
||||
from .reports import report_view
|
||||
|
||||
import bleach
|
||||
|
||||
|
||||
def CURRENCY():
|
||||
return models.DecimalField(decimal_places=2)
|
||||
|
@ -240,6 +244,83 @@ def group_by_cart_status(queryset, order, 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)
|
||||
def product_status(request, form):
|
||||
''' Summarises the inventory status of the given items, grouping by
|
||||
|
@ -329,7 +410,7 @@ def product_line_items(request, form):
|
|||
"user",
|
||||
"user__attendee",
|
||||
"user__attendee__attendeeprofilebase"
|
||||
).order_by("issue_time")
|
||||
).order_by("issue_time").distinct()
|
||||
|
||||
headings = [
|
||||
'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 = invoices.filter(value=0)
|
||||
zero_value_invoices = invoices.filter(value=0).distinct()
|
||||
|
||||
times = itertools.chain(
|
||||
(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:
|
||||
return attendee_list(request)
|
||||
|
||||
attendee = people.Attendee.objects.get(user__id=user_id)
|
||||
name = attendee.attendeeprofilebase.attendee_name()
|
||||
|
||||
reports = []
|
||||
|
||||
profile_data = []
|
||||
try:
|
||||
attendee = people.Attendee.objects.get(user__id=user_id)
|
||||
name = attendee.attendeeprofilebase.attendee_name()
|
||||
|
||||
profile = people.AttendeeProfileBase.objects.get_subclass(
|
||||
attendee=attendee
|
||||
)
|
||||
fields = profile._meta.get_fields()
|
||||
except people.AttendeeProfileBase.DoesNotExist:
|
||||
name = attendee.user.username
|
||||
fields = []
|
||||
|
||||
exclude = set(["attendeeprofilebase_ptr", "id"])
|
||||
|
@ -489,11 +571,17 @@ def attendee(request, form, user_id=None):
|
|||
|
||||
if isinstance(field, models.ManyToManyField):
|
||||
value = ", ".join(str(i) for i in value.all())
|
||||
elif isinstance(field, CharField):
|
||||
value = bleach.clean(value)
|
||||
|
||||
profile_data.append((field.verbose_name, value))
|
||||
|
||||
cart = CartController.for_user(attendee.user)
|
||||
reservation = cart.cart.reservation_duration + cart.cart.time_last_updated
|
||||
try:
|
||||
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))
|
||||
|
||||
reports.append(ListReport("Profile", ["", ""], profile_data))
|
||||
|
@ -515,53 +603,65 @@ def attendee(request, form, user_id=None):
|
|||
reports.append(Links("Actions for " + name, links))
|
||||
|
||||
# Paid and pending products
|
||||
ic = ItemController(attendee.user)
|
||||
reports.append(ListReport(
|
||||
"Paid Products",
|
||||
["Product", "Quantity"],
|
||||
[(pq.product, pq.quantity) for pq in ic.items_purchased()],
|
||||
))
|
||||
reports.append(ListReport(
|
||||
"Unpaid Products",
|
||||
["Product", "Quantity"],
|
||||
[(pq.product, pq.quantity) for pq in ic.items_pending()],
|
||||
))
|
||||
try:
|
||||
ic = ItemController(attendee.user)
|
||||
reports.append(ListReport(
|
||||
"Paid Products",
|
||||
["Product", "Quantity"],
|
||||
[(pq.product, pq.quantity) for pq in ic.items_purchased()],
|
||||
))
|
||||
reports.append(ListReport(
|
||||
"Unpaid Products",
|
||||
["Product", "Quantity"],
|
||||
[(pq.product, pq.quantity) for pq in ic.items_pending()],
|
||||
))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Invoices
|
||||
invoices = commerce.Invoice.objects.filter(
|
||||
user=attendee.user,
|
||||
)
|
||||
reports.append(QuerysetReport(
|
||||
"Invoices",
|
||||
["id", "get_status_display", "value"],
|
||||
invoices,
|
||||
headings=["Invoice ID", "Status", "Value"],
|
||||
link_view=views.invoice,
|
||||
))
|
||||
try:
|
||||
invoices = commerce.Invoice.objects.filter(
|
||||
user=attendee.user,
|
||||
)
|
||||
reports.append(QuerysetReport(
|
||||
"Invoices",
|
||||
["id", "get_status_display", "value"],
|
||||
invoices,
|
||||
headings=["Invoice ID", "Status", "Value"],
|
||||
link_view=views.invoice,
|
||||
))
|
||||
except AttrbuteError:
|
||||
pass
|
||||
|
||||
# Credit Notes
|
||||
credit_notes = commerce.CreditNote.objects.filter(
|
||||
invoice__user=attendee.user,
|
||||
).select_related("invoice", "creditnoteapplication", "creditnoterefund")
|
||||
try:
|
||||
credit_notes = commerce.CreditNote.objects.filter(
|
||||
invoice__user=attendee.user,
|
||||
).select_related("invoice", "creditnoteapplication", "creditnoterefund")
|
||||
|
||||
reports.append(QuerysetReport(
|
||||
"Credit Notes",
|
||||
["id", "status", "value"],
|
||||
credit_notes,
|
||||
link_view=views.credit_note,
|
||||
))
|
||||
reports.append(QuerysetReport(
|
||||
"Credit Notes",
|
||||
["id", "status", "value"],
|
||||
credit_notes,
|
||||
link_view=views.credit_note,
|
||||
))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# All payments
|
||||
payments = commerce.PaymentBase.objects.filter(
|
||||
invoice__user=attendee.user,
|
||||
).select_related("invoice")
|
||||
try:
|
||||
payments = commerce.PaymentBase.objects.filter(
|
||||
invoice__user=attendee.user,
|
||||
).select_related("invoice")
|
||||
|
||||
reports.append(QuerysetReport(
|
||||
"Payments",
|
||||
["invoice__id", "id", "reference", "amount"],
|
||||
payments,
|
||||
link_view=views.invoice,
|
||||
))
|
||||
reports.append(QuerysetReport(
|
||||
"Payments",
|
||||
["invoice__id", "id", "reference", "amount"],
|
||||
payments,
|
||||
link_view=views.invoice,
|
||||
))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return reports
|
||||
|
||||
|
@ -678,6 +778,7 @@ def attendee_data(request, form, user_id=None):
|
|||
category = product + "__category"
|
||||
category_name = category + "__name"
|
||||
|
||||
|
||||
if by_category:
|
||||
grouping_fields = (category, category_name)
|
||||
order_by = (category, )
|
||||
|
@ -712,7 +813,7 @@ def attendee_data(request, form, user_id=None):
|
|||
return None
|
||||
else:
|
||||
def display_field(value):
|
||||
return value
|
||||
return bleach.clean(value)
|
||||
|
||||
status_count = lambda status: Case(When( # noqa
|
||||
attendee__user__cart__status=status,
|
||||
|
@ -759,7 +860,7 @@ def attendee_data(request, form, user_id=None):
|
|||
if isinstance(field_type, models.ManyToManyField):
|
||||
return [str(i) for i in attr.all()] or ""
|
||||
else:
|
||||
return attr
|
||||
return bleach.clean(attr)
|
||||
|
||||
headings = ["User ID", "Name", "Email", "Product", "Item Status"]
|
||||
headings.extend(field_names)
|
||||
|
|
|
@ -10,6 +10,8 @@ try:
|
|||
except ImportError:
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from operator import attrgetter
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
|
@ -46,8 +48,9 @@ def missing_categories(context):
|
|||
for product, quantity in items:
|
||||
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)
|
||||
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 (
|
||||
amend_registration,
|
||||
badge,
|
||||
badges,
|
||||
badger,
|
||||
checkout,
|
||||
credit_note,
|
||||
edit_profile,
|
||||
|
@ -26,7 +26,8 @@ from .views import (
|
|||
public = [
|
||||
url(r"^amend/([0-9]+)$", amend_registration, name="amend_registration"),
|
||||
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"^checkout$", checkout, name="checkout"),
|
||||
url(r"^checkout/([0-9]+)$", checkout, name="checkout"),
|
||||
|
@ -42,9 +43,9 @@ public = [
|
|||
name="invoice_access"),
|
||||
url(r"^invoice_mailout$", invoice_mailout, name="invoice_mailout"),
|
||||
url(r"^profile$", edit_profile, name="attendee_edit"),
|
||||
url(r"^register$", guided_registration, name="guided_registration"),
|
||||
url(r"^review$", review, name="review"),
|
||||
url(r"^voucher$", voucher_code, name="voucher_code"),
|
||||
url(r"^register$", guided_registration, name="guided_registration"),
|
||||
url(r"^register/([0-9]+)$", guided_registration,
|
||||
name="guided_registration"),
|
||||
]
|
||||
|
@ -56,6 +57,7 @@ reports = [
|
|||
url(r"^attendee_data/?$", rv.attendee_data, name="attendee_data"),
|
||||
url(r"^attendee/([0-9]*)$", rv.attendee, name="attendee"),
|
||||
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"^product_line_items/?$",
|
||||
|
|
166
vendor/registrasion/registrasion/views.py
vendored
166
vendor/registrasion/registrasion/views.py
vendored
|
@ -1,5 +1,6 @@
|
|||
import datetime
|
||||
import zipfile
|
||||
import os
|
||||
|
||||
from . import forms
|
||||
from . import util
|
||||
|
@ -32,6 +33,16 @@ from django.shortcuts import redirect
|
|||
from django.shortcuts import render
|
||||
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",
|
||||
|
@ -544,11 +555,13 @@ def _handle_products(request, category, products, prefix):
|
|||
# If category is required, the user must have at least one
|
||||
# in an active+valid cart
|
||||
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(
|
||||
product__category=category,
|
||||
cart=carts,
|
||||
cart=current_cart.cart,
|
||||
)
|
||||
|
||||
if len(items) == 0:
|
||||
products_form.add_error(
|
||||
None,
|
||||
|
@ -1076,24 +1089,42 @@ def invoice_mailout(request):
|
|||
|
||||
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)
|
||||
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 = User.objects.get(pk=user_id)
|
||||
|
||||
rendered = render_badge(user)
|
||||
response = HttpResponse(rendered)
|
||||
# This will fail spectacularly -- will put exception handling in later ...
|
||||
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-Disposition"] = 'inline; filename="badge.svg"'
|
||||
return response
|
||||
|
||||
|
||||
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. '''
|
||||
|
||||
category = request.GET.getlist("category", [])
|
||||
|
@ -1128,12 +1159,121 @@ def badges(request):
|
|||
return render(request, "registrasion/badges.html", data)
|
||||
|
||||
|
||||
def render_badge(user):
|
||||
''' Renders a single user's badge. '''
|
||||
def collate_from_form(form):
|
||||
'''
|
||||
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 = {
|
||||
"user": user,
|
||||
}
|
||||
# Get the name bits ...
|
||||
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')
|
||||
return t.render(data)
|
||||
# Free text -- only one line ... come on!
|
||||
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