Merge branch 'ticket-testing' into 'master'

Ready to go live *gulp*

See merge request LCA2018/symposion_app!51
This commit is contained in:
James Polley 2017-10-01 13:36:22 +00:00
commit c0f839c0dd
193 changed files with 4019 additions and 2461 deletions

View file

@ -80,9 +80,10 @@ version of pip than is packaged with distros virtualenv.
Note that this application is python 3 only so you must create your virtualenv
with a python3 interpreter.
- ``virtualenv -p `which python3` venv``
- ``python3 -m venv venv``
- ``source ./venv/bin/activate``
- ``pip install -c constraints.txt -r requirements.txt``
- ``pip install -c constraints.txt -r vendored_requirements.txt``
Once your dev instance is up and running
----------------------------------------
@ -138,7 +139,7 @@ admin3:XzynbNH9Sw3pLPXe
Creating review permissions objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Some more voodoo magic that needs to be manually run because that's just how symposion works.
This creates the permission that needs to be applied to a user/group/team to be able to see the review sections of the site.
After conference Sections have been created, this command will add
Permission objects for those sections.
``./manage.py create_review_permissions``

View file

@ -1,6 +1,8 @@
FROM python:3.6
COPY constraints.txt requirements.txt /reqs/
RUN set -ex \
&& apt-get update
RUN set -ex \
&& buildDeps=' \
@ -15,18 +17,26 @@ RUN set -ex \
libmemcached-dev \
libsasl2-dev \
' \
&& apt-get update \
&& apt-get install -y git xmlsec1 libmysqlclient18 \
&& apt-get install -y $buildDeps --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /var/lib/apt/lists/*
RUN set -ex \
&& pip install uwsgi
COPY constraints.txt requirements.txt /reqs/
RUN set -ex \
&& pip install --no-cache-dir -r /reqs/requirements.txt -c /reqs/constraints.txt \
&& pip install uwsgi \
&& apt-get purge -y --auto-remove $buildDeps \
&& rm -rf /usr/src/python ~/.cache
COPY . /app/symposion_app
WORKDIR /app/symposion_app
RUN set -x \
&& pip install -r vendored_requirements.txt -c /reqs/constraints.txt
RUN set -x \
&& DJANGO_SECRET_KEY=1234 STRIPE_PUBLIC_KEY=1234 STRIPE_SECRET_KEY=1234 \
DATABASE_URL="sqlite:////dev/null" \

View file

@ -18,5 +18,8 @@ RUN set -ex \
&& apt-get install -y git xmlsec1 libmysqlclient18 \
&& apt-get install -y $buildDeps --no-install-recommends
RUN pip install -c /setup/constraints.txt -r /setup/requirements.txt
COPY . /source
WORKDIR /source
RUN pip install -c /setup/constraints.txt -r /source/vendored_requirements.txt
ENTRYPOINT ["python","/source/manage.py", "makemigrations"]

View file

@ -7,7 +7,7 @@ submission, reviews, scheduling and sponsor management.
.. toctree::
:maxdepth: 2
:maxdepth: 3
project
conference
@ -17,12 +17,6 @@ submission, reviews, scheduling and sponsor management.
speakers
schedule
miniconfs
.. include:: registrasion/README.rst
.. toctree::
:maxdepth: 3
registrasion/index.rst
About

View file

@ -0,0 +1,5 @@
from django.contrib import admin
from .models import PastEvent
admin.site.register(PastEvent)

View file

@ -9,7 +9,7 @@ class YesNoField(forms.TypedChoiceField):
kwargs['required'] = True
super(YesNoField, self).__init__(
*args,
coerce=lambda x: x is True,
coerce=lambda x: x in ['True', 'Yes', True],
choices=((None, '--------'), (False, 'No'), (True, 'Yes')),
**kwargs
)

View file

@ -13,7 +13,7 @@ from symposion import proposals
class Command(BaseCommand):
help = 'Populates the inventory with the LCA2017 inventory model'
help = 'Populates the inventory with the LCA2018 inventory model'
def add_arguments(self, parser):
pass
@ -67,6 +67,20 @@ class Command(BaseCommand):
limit_per_user=1,
order=1,
)
self.terms = self.find_or_make(
inv.Category,
("name",),
name="Terms, Conditions, and Code of Conduct Acceptance",
description="I agree to the "
"<a href=\"https://linux.conf.au/attend/terms-and-conditions\"> "
"terms and conditions of attendance</a>, and I have read, "
"understood, and agree to act according to the standards set "
"forth in our <a href=\"https://linux.conf.au/attend/code-of-conduct\">"
"Code of Conduct</a>.",
required=True,
render_type=inv.Category.RENDER_TYPE_CHECKBOX,
order=10,
)
self.penguin_dinner = self.find_or_make(
inv.Category,
("name",),
@ -78,7 +92,7 @@ class Command(BaseCommand):
required=False,
render_type=inv.Category.RENDER_TYPE_QUANTITY,
limit_per_user=10,
order=10,
order=20,
)
self.speakers_dinner_ticket = self.find_or_make(
inv.Category,
@ -91,7 +105,7 @@ class Command(BaseCommand):
required=False,
render_type=inv.Category.RENDER_TYPE_QUANTITY,
limit_per_user=5,
order=20,
order=30,
)
self.pdns_category = self.find_or_make(
inv.Category,
@ -105,17 +119,17 @@ class Command(BaseCommand):
required=False,
render_type=inv.Category.RENDER_TYPE_RADIO,
limit_per_user=1,
order=30,
order=40,
)
self.t_shirt = self.find_or_make(
inv.Category,
("name",),
name="T-Shirt",
description="Commemorative conference t-shirts, featuring the"
name="Shirt",
description="Commemorative conference polo shirts, featuring the "
"linux.conf.au 2018 artwork.",
required=False,
render_type=inv.Category.RENDER_TYPE_ITEM_QUANTITY,
order=40,
order=50,
)
# self.accommodation = self.find_or_make(
# inv.Category,
@ -258,6 +272,17 @@ class Command(BaseCommand):
order=90,
)
# Agreements
self.accept_terms = self.find_or_make(
inv.Product,
("name","category",),
category = self.terms,
name="I Accept",
price=Decimal("00.00"),
reservation_duration=hours(24),
order=10,
limit_per_user=1,
)
# Penguin dinner
self.penguin_adult = self.find_or_make(
@ -358,7 +383,7 @@ class Command(BaseCommand):
inv.Product,
("name", "category",),
category=self.extras,
name="Offset the carbon polution generated by your attendance, "
name="Offset the carbon pollution generated by your attendance, "
"thanks to fifteen trees.",
price=Decimal("5.00"),
reservation_duration=hours(1),
@ -369,12 +394,12 @@ class Command(BaseCommand):
ShirtGroup = namedtuple("ShirtGroup", ("prefix", "sizes"))
shirt_names = {
"mens": ShirtGroup(
"Men's/Straight Cut Size",
("S", "M", "L", "XL", "2XL", "3XL", "5XL"),
"Men's/Straight Cut",
("S", "M", "L", "XL", "2XL", "3XL", "4XL"),
),
"womens": ShirtGroup(
"Women's Classic Fit",
("XS", "S", "M", "L", "XL", "2XL"),
("8", "10", "12", "14", "16", "18"),
),
}
@ -621,7 +646,6 @@ class Command(BaseCommand):
)
pdns_by_staff.group.set([
self.group_team,
self.group_volunteers,
])
pdns_by_staff.categories.set([self.pdns_category, ])
@ -630,14 +654,30 @@ class Command(BaseCommand):
cond.CategoryFlag,
("description", ),
description="GottaGettaTicketFirst",
condition=cond.FlagBase.ENABLE_IF_TRUE,
condition=cond.FlagBase.DISABLE_IF_FALSE,
enabling_category = self.ticket
)
needs_a_ticket.categories.set([
self.extras,
self.t_shirt,
self.penguin_dinner,
self.pdns_category,
])
# Require attendees to accept the T&Cs and Code of Conduct
needs_agreement = self.find_or_make(
cond.CategoryFlag,
("description", ),
description="Must Accept Terms",
condition=cond.FlagBase.DISABLE_IF_FALSE,
enabling_category = self.terms,
)
needs_agreement.categories.set([
self.extras,
self.t_shirt,
self.penguin_dinner,
self.pdns_category,
])
def populate_discounts(self):
@ -673,7 +713,7 @@ class Command(BaseCommand):
early_bird_hobbyist_discount = self.find_or_make(
cond.TimeOrStockLimitDiscount,
("description", ),
description="Early Bird Hobbyist",
description="Early Bird Discount - Hobbyist",
end_time=datetime(year=2017, month=11, day=1),
limit=150, # Across all users
)
@ -689,9 +729,9 @@ class Command(BaseCommand):
early_bird = self.find_or_make(
cond.TimeOrStockLimitDiscount,
("description", ),
description="Early Bird",
description="Early Bird Discount - Professional",
end_time=datetime(year=2017, month=11, day=1),
limit=200, # Across professionals and hobbyists
limit=200, # Across professionals and fairy sponsors
)
add_early_birds(early_bird)
@ -757,23 +797,22 @@ class Command(BaseCommand):
])
free_category(ticket_student_inclusions, self.t_shirt)
# Team & volunteer ticket inclusions
# Team ticket inclusions
ticket_staff_inclusions = self.find_or_make(
cond.IncludedProductDiscount,
("description", ),
description="Complimentary for ticket holder (staff/volunteer)",
description="Complimentary for ticket holder staff)",
)
ticket_staff_inclusions.enabling_products.set([
self.ticket_team,
self.ticket_volunteer,
])
free_category(ticket_staff_inclusions, self.penguin_dinner)
# Team & volunteer t-shirts, regardless of ticket type
# Team & volunteer shirts, regardless of ticket type
staff_t_shirts = self.find_or_make(
cond.GroupMemberDiscount,
("description", ),
description="T-shirts complimentary for staff and volunteers",
description="Shirts complimentary for staff and volunteers",
)
staff_t_shirts.group.set([
self.group_team,

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-27 13:01
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pinaxcon_registrasion', '0005_auto_20170702_2233'),
]
operations = [
migrations.AddField(
model_name='attendeeprofile',
name='future_conference',
field=models.BooleanField(default=False, help_text='Select to have your login details made available to future Linux Australia conferences who share the same Single Sign On system.', verbose_name='Reuse my login for future Linux Australia conferences?'),
preserve_default=False,
),
migrations.AlterField(
model_name='attendeeprofile',
name='lca_chat',
field=models.BooleanField(help_text='lca2018-chat is a high-traffic mailing list used by attendees during the week of the conference for general discussion.', verbose_name='Subscribe to the lca2018-chat list'),
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-30 06:10
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pinaxcon_registrasion', '0006_auto_20170927_2301'),
]
operations = [
migrations.AddField(
model_name='attendeeprofile',
name='agreement',
field=models.BooleanField(default=False, help_text='I agree to the <a href="https://linux.conf.au/attend/terms-and-conditions"> terms and conditions of attendance</a>, and I have read, understood, and agree to act according to the standards set forth in our <a href="https://linux.conf.au/attend/code-of-conduct">Code of Conduct</a>.'),
preserve_default=False,
),
migrations.AlterField(
model_name='attendeeprofile',
name='state',
field=models.CharField(blank=True, help_text='If your Country is Australia, you must list a state.', max_length=256, verbose_name='State/Territory/Province'),
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-10-01 08:14
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('pinaxcon_registrasion', '0007_auto_20170930_1610'),
]
operations = [
migrations.RemoveField(
model_name='attendeeprofile',
name='agreement',
),
]

View file

@ -138,6 +138,7 @@ class AttendeeProfile(rego.AttendeeProfileBase):
state = models.CharField(
max_length=256,
verbose_name="State/Territory/Province",
help_text="If your Country is Australia, you must list a state.",
blank=True,
)
@ -179,13 +180,21 @@ class AttendeeProfile(rego.AttendeeProfileBase):
)
lca_chat = models.BooleanField(
verbose_name="Subscribe to the lca2017-chat list",
help_text="lca2017-chat is a high-traffic mailing list used by "
verbose_name="Subscribe to the lca2018-chat list",
help_text="lca2018-chat is a high-traffic mailing list used by "
"attendees during the week of the conference for general "
"discussion.",
blank=True,
)
future_conference = models.BooleanField(
verbose_name = "Reuse my login for future Linux Australia conferences?",
help_text="Select to have your login details made available to future "
"Linux Australia conferences who share the same Single Sign "
"On system.",
blank=True,
)
past_lca = models.ManyToManyField(
PastEvent,
verbose_name="Which past linux.conf.au events have you attended?",

View file

@ -322,7 +322,7 @@ PROPOSAL_FORMS = {
ATTENDEE_PROFILE_MODEL = "pinaxcon.registrasion.models.AttendeeProfile"
ATTENDEE_PROFILE_FORM = "pinaxcon.registrasion.forms.ProfileForm"
INVOICE_CURRENCY = "AUD"
TICKET_PRODUCT_CATEGORY = 1
ATTENDEE_PROFILE_FORM = "pinaxcon.registrasion.forms.ProfileForm"
# CSRF custom error screen

View file

@ -1,4 +1,4 @@
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load bootstrap %}
{% if form.non_field_errors %}

View file

@ -1,7 +1,7 @@
{% extends "site_base.html" %}
{% load staticfiles %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load i18n %}

View file

@ -5,7 +5,7 @@
{% load review_tags %}
{% load teams_tags %}
{% load registrasion_tags %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load staticfiles %}
@ -42,8 +42,8 @@
<h4>Register</h4>
</div>
<div class="panel-body">
<p>To attend the conference, you must register an attendee profile and purchase your ticket</p>
<a class="btn btn-lg btn-primary" role="button" href="{% url "guided_registration" %}">Get your ticket</a>
<p>To attend the conference, you must create an attendee profile and purchase your ticket</p>
<a class="btn btn-lg btn-success" role="button" href="{% url "guided_registration" %}">Get your ticket</a>
</div>
</div>
{% else %}
@ -125,9 +125,9 @@
<a href="{% url "invoice" invoice.id %}" >Invoice {{ invoice.id }}</a>
- ${{ invoice.value }} ({{ invoice.get_status_display }})
</li>
<a id="toggle-void-invoices" href="" onclick="toggleVoidInvoices();">Show void invoices</a>
{% endfor %}
</ul>
<button id="toggle-void-invoices" onclick="toggleVoidInvoices();">Show void invoices</button>
</div>
</div>
</div>

View file

@ -1,12 +1,12 @@
{% load registrasion_tags %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
<h2>Tax Invoice/Statement</h2>
<h3>Linux Australia</h3>
<h4>ABN 56 987 117 479</h4>
<p>
Enquiries: please e-mail <a href="mailto:team@hobart.pyconau2017.org">team@hobart.pyconau2017.org</a>
Enquiries: please e-mail <a href="mailto:team@lca2018.org">team@lca2018.org</a>
</p>
<ul>

View file

@ -1,5 +1,9 @@
{% if discounts %}
<div class="alert-success">
<h3 class="label-success">Discounts and Complimentary Items</h3>
<div class="vertical-small"></div>
<blockquote>The following discounts and complimentary items are available to you. If you wish to take advantage of this offer, you must choose your items below. This discounts will be applied automatically when you check out.</blockquote>
{% regroup discounts by discount.description as discounts_grouped %}
{% for discount_type in discounts_grouped %}
<h4>{{ discount_type.grouper }}</h4>
@ -9,4 +13,8 @@
{% endfor %}
</ul>
{% endfor %}
<hr />
</div>
{% endif %}

View file

@ -1,5 +1,5 @@
{% extends "registrasion/base.html" %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% block header_title %}Buy Your Ticket{% endblock %}
{% block header_paragraph %}Step {{ current_step }} of {{ total_steps|add:1 }} &ndash; {{ title }} {% endblock %}
@ -8,6 +8,21 @@
{% for section in sections %}
{{ section.form.media.js }}
{% endfor %}
<script type="text/javascript">
postcode_label = $("label[for='id_profile-state']");
postcode_help = $("#id_profile-state + p");
$('#id_profile-country').change(function () {
if ($(this).val() == 'AU' ) {
postcode_label.addClass('label-required');
postcode_help.show();
} else {
postcode_label.removeClass('label-required');
postcode_help.hide();
} });
$("#id_profile-country").change();
</script>
{% endblock %}
{% block content %}
@ -26,14 +41,6 @@
{% if section.discounts %}
{% include "registrasion/discount_list.html" with discounts=section.discounts %}
<blockquote><small>
You must select a product to receive any discounts.<br/>
Applicable discounts will be applied automatically when you check out.
</small></blockquote>
<hr />
{% endif %}
<h3>Available options</h3>
@ -45,7 +52,10 @@
{% endfor %}
<div class="btn-group">
<input class="btn btn-primary" type="submit" value="Next Step" />
{% if current_step > 1 %}
<a class="btn btn-primary" role="button" href="{{ previous_step }}">Back</a>
{% endif %}
<input class="btn btn-success" type="submit" value="Next Step" />
</div>
</form>

View file

@ -1,15 +1,15 @@
{% extends "registrasion/base.html" %}
{% load registrasion_tags %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load staticfiles %}
{% block header_title %}{% conference_name %}{% endblock %}
{% block header_paragraph %}
<p>Monday 16 January&ndash;Friday 20 January 2017.</p>
<p>Wrest Point Convention Centre, Hobart, Tasmania, Australia.</p>
<p>Monday 22 January&ndash;Friday 26 January 2018.</p>
<p>University of Technology Sydney, New South Wales, Australia.</p>
{% endblock %}
{% block header_inset_image %}{% illustration "tuz.svg" %}{% endblock %}
{% block header_background_image %}{% static "pyconau2017/images/wp_bg_optimised.jpg" %}{% endblock %}
{% block header_background_image %}{% static "lca2018/images/wp_bg_optimised.jpg" %}{% endblock %}
{% block content %}
@ -25,7 +25,7 @@
{% url "invoice_access" invoice.user.attendee.access_code as access_url %}
<p>Your most recent unpaid invoice will be available at
<a href="{{ access_url }}">{{ request.scheme }}://{{ request.get_host }}{{ access_url }}</a>
You can give this URL to your accounts department to pay for this invoice.</p>
You can print that page for your records, or give this URL to your accounts department to pay for this invoice</p>
<div class="btn-group">
<a class="btn btn-default" href='{% url "registripe_card" invoice.id invoice.user.attendee.access_code %}'>Pay this invoice by card</a>

View file

@ -1,6 +1,6 @@
{% extends "registrasion/base.html" %}
{% load registrasion_tags %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% block header_title %}Product Category: {{ category.name }}{% endblock %}
{% block header_inset_image %}{% illustration "lavender.svg" %}{% endblock %}
@ -50,13 +50,7 @@
<fieldset>
{% if discounts %}
<h3>Discounts and Complimentary Items</h3>
<div class="vertical-small"></div>
{% include "registrasion/discount_list.html" with discounts=discounts %}
<blockquote><small>Any applicable discounts will be applied automatically when you check out.</small></blockquote>
<hr />
{% endif %}
<h3>Make a selection</h3>

View file

@ -1,5 +1,5 @@
{% extends "registrasion/base.html" %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% block header_title %}Your profile{% endblock %}
{% block header_paragraph %}
@ -8,13 +8,25 @@
{% endblock %}
{% block scripts_extra %}
{{ form.media.js }}
<script type="text/javascript">
</script>
{{ form.media.js }}
<script type="text/javascript">
postcode_label = $("label[for='id_profile-state']");
postcode_help = $("#id_profile-state + p");
$('#id_profile-country').change(function () {
if ($(this).val() == 'AU' ) {
postcode_label.addClass('label-required');
postcode_help.show();
} else {
postcode_label.removeClass('label-required');
postcode_help.hide();
} });
$("#id_profile-country").change();
</script>
{% endblock %}
{% block content %}
THIS IS THE FORM
<form class="form-horizontal" method="post" action="">
{% csrf_token %}

View file

@ -1,6 +1,6 @@
{% extends "registrasion/base.html" %}
{% load registrasion_tags %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% block header_title %}Review your selection{% endblock %}
{% block header_inset_image %}{% illustration "wineglass.svg" %}{% endblock %}
@ -33,8 +33,8 @@
{% missing_categories as missing %}
{% if missing %}
<p>
<div class="alert-warning">
<p class="label-warning">
<strong>You have <em>not</em> selected anything from the following
categories. If your ticket includes any of these, you still need to
make a selection:
@ -42,7 +42,7 @@
</p>
{% include "registrasion/_category_list.html" with categories=missing %}
</div>
{% endif %}
<p>
@ -58,7 +58,7 @@
the dashboard.</p>
<div class="btn-group">
<a class="btn" href="{% url "checkout" %}">
<a class="btn btn-success" href="{% url "checkout" %}">
<i class="fa fa-credit-card"></i> Check out and pay
</a>
<a class="btn btn-primary" href="{% url "dashboard" %}">Return to dashboard</a>

View file

@ -1,6 +1,6 @@
{% extends "registrasion/base.html" %}
{% load registrasion_tags %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% block scripts %}

View file

@ -0,0 +1,68 @@
var stripe = Stripe('{{ PINAX_STRIPE_PUBLIC_KEY }}');
var elements = stripe.elements();
function stripeify(elementId) {
var element = elements.create(elementId);
element.mount('#' + elementId);
var htmlElement = document.getElementById(elementId);
var errors = elementId + "-errors";
htmlElement.insertAdjacentHTML("afterend", "<div id='" + errors + "' role='alert' class='help-block'></div>");
var displayError = document.getElementById(errors);
//Handle real-time validation errors from the card Element.
element.addEventListener('change', function(event) {
toggleErrorMessage(displayError, event.error);
});
// Create a token or display an error when the form is submitted.
var paymentForm = document.getElementById('payment-form');
paymentForm.addEventListener('submit', function(event) {
event.preventDefault();
stripe.createToken(element).then(function(result) {
if (result.error) {
// Inform the user if there was an error
toggleErrorMessage(displayError, result.error);
} else {
// Send the token to your server
stripeTokenHandler(result.token);
}
});
});
}
function toggleErrorMessage(errorElement, maybeError) {
errorClass = inputErrorClassName();
if (maybeError) {
errorElement.textContent = maybeError.message;
errorElement.parentNode.classList.add(errorClass);
} else {
errorElement.textContent = '';
errorElement.parentNode.classList.remove(errorClass);
}
}
function inputErrorClassName() {
return {% block form_control_error_class %}"has-error"{% endblock %};
}
function stripeTokenHandler(token) {
// Insert the token ID into the form so it gets submitted to the server
var form = document.getElementById('payment-form');
tokenHolder = form.getElementsByClassName('registrasion-stripe-token')[0];
inputId = tokenHolder.dataset.inputId;
var hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', inputId);
hiddenInput.setAttribute('value', token.id);
tokenHolder.appendChild(hiddenInput);
// Submit the form
form.submit();
}

View file

@ -1,5 +1,5 @@
{% load i18n %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
<div>
<label class="col-sm-2 col-lg-2">Submitted by</label>

View file

@ -1,4 +1,4 @@
{% load pyconau2017_tags %}
{% load lca2018_tags %}
<table class="calendar table table-bordered">
<thead>
<tr>

View file

@ -1,6 +1,6 @@
{% extends "symposion/schedule/public_base.html" %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load sitetree %}
{% load staticfiles %}
{% load thumbnail %}
@ -11,7 +11,7 @@
{% block header_inset_image %}{% with audience=presentation.proposal.get_target_audience_display %}{% if audience == "Business" %}{% illustration "falls.svg" %}{% elif audience == "Community" %}{% illustration "bridge.svg" %}{% elif audience == "Developer"%}{% illustration "hobart.svg" %}{% elif audience == "User" %}{% illustration "antarctica.svg" %}{% else %}{% illustration "casino.svg" %}{% endif %}{% endwith %}{% endblock %}
{% block header_background_image %}{% presentation_bg_number presentation 4 as bg_number %}{% if bg_number == 0 %}{% static "pyconau2017/images/mt_anne_bg_optimised.jpg" %}{% elif bg_number == 1 %}{% static "pyconau2017/images/the_neck_bg_optimised.jpg" %}{% elif bg_number == 2 %}{% static "pyconau2017/images/snug_falls_bg_optimised.jpg" %}{% elif bg_number == 3 %}{% static "pyconau2017/images/sleepy_bay_bg_optimised.jpg" %}{% endif %}{% endblock %}
{% block header_background_image %}{% presentation_bg_number presentation 4 as bg_number %}{% if bg_number == 0 %}{% static "lca2018/images/mt_anne_bg_optimised.jpg" %}{% elif bg_number == 1 %}{% static "lca2018/images/the_neck_bg_optimised.jpg" %}{% elif bg_number == 2 %}{% static "lca2018/images/snug_falls_bg_optimised.jpg" %}{% elif bg_number == 3 %}{% static "lca2018/images/sleepy_bay_bg_optimised.jpg" %}{% endif %}{% endblock %}
{% block header_title %}{{ presentation.title }}{% endblock %}

View file

@ -3,5 +3,5 @@
{% load staticfiles %}
{% comment %}
{% block header_background_image %}{% static "pyconau2017/images/hobart_bg_optimised.jpg" %}{% endblock %}
{% block header_background_image %}{% static "lca2018/images/hobart_bg_optimised.jpg" %}{% endblock %}
{% endcomment %}

View file

@ -2,7 +2,7 @@
{% load i18n %}
{% load cache %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% block head_title %}Conference Schedule{% endblock %}
{% block header_title %}Conference Schedule{% endblock %}
@ -57,16 +57,16 @@
fragment = window.location.hash.toLowerCase().substring(1);
if (!fragment) {
OFFSET = -11 * (60 * 60 * 1000); // Hobart is 11 hours ahead of UTC in Jan.
OFFSET = -11 * (60 * 60 * 1000); // Sydney is 11 hours ahead of UTC in Jan.
JAN = 0; // because January is 0, not 1
fragments = [
{"day": "monday", "time": Date.UTC(2017, JAN, 16)},
{"day": "tuesday", "time": Date.UTC(2017, JAN, 17)},
{"day": "wednesday", "time": Date.UTC(2017, JAN, 18)},
{"day": "thursday", "time": Date.UTC(2017, JAN, 19)},
{"day": "friday", "time": Date.UTC(2017, JAN, 20)},
{"day": "saturday", "time": Date.UTC(2017, JAN, 21)},
{"day": "monday", "time": Date.UTC(2018, JAN, 22)},
{"day": "tuesday", "time": Date.UTC(2018, JAN, 23)},
{"day": "wednesday", "time": Date.UTC(2018, JAN, 24)},
{"day": "thursday", "time": Date.UTC(2018, JAN, 25)},
{"day": "friday", "time": Date.UTC(2018, JAN, 26)},
{"day": "saturday", "time": Date.UTC(2018, JAN, 27)},
];
now = new Date().getTime();

View file

@ -2,7 +2,7 @@
{% load i18n %}
{% load cache %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load sitetree %}
{% block head_title %}{{ schedule.section }} Schedule{% endblock %}

View file

@ -2,7 +2,7 @@
{% load i18n %}
{% load cache %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load sitetree %}
{% block head_title %}Presentation Listing{% endblock %}

View file

@ -1,7 +1,7 @@
{% extends "symposion/schedule/public_base.html" %}
{% load i18n %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load thumbnail %}
{% if speaker.photo %}

View file

@ -1,4 +1,4 @@
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% spaceless %}
<a href="{{ sponsor.external_url }}">

View file

@ -1,7 +1,7 @@
{% extends "site_base.html" %}
{% load sponsorship_tags %}
{% load pyconau2017_tags %}
{% load lca2018_tags %}
{% load i18n %}
{% block head_title %}{% trans "About Our Sponsors" %}{% endblock %}

View file

@ -34,11 +34,9 @@ def proposal_permission(context, permname, proposal):
return context.request.user.has_perm(perm)
# {% load statictags %}{% static 'pyconau2017/images/svgs/illustrations/' %}{{ illustration }}
@register.simple_tag(takes_context=False)
def illustration(name):
return staticfiles.static('pyconau2017/images/svgs/illustrations/') + name
return staticfiles.static('lca2018/images/svgs/illustrations/') + name
@register.simple_tag(takes_context=True)

View file

@ -1,100 +0,0 @@
import hashlib
import os
from decimal import Decimal
from django import template
from django.conf import settings
from django.contrib.staticfiles.templatetags import staticfiles
from easy_thumbnails.files import get_thumbnailer
from symposion.conference import models as conference_models
from symposion.schedule.models import Track
CONFERENCE_ID = settings.CONFERENCE_ID
register = template.Library()
@register.assignment_tag()
def classname(ob):
return ob.__class__.__name__
@register.simple_tag(takes_context=True)
def can_manage(context, proposal):
return proposal_permission(context, "manage", proposal)
@register.simple_tag(takes_context=True)
def can_review(context, proposal):
return proposal_permission(context, "review", proposal)
def proposal_permission(context, permname, proposal):
slug = proposal.kind.section.slug
perm = "reviews.can_%s_%s" % (permname, slug)
return context.request.user.has_perm(perm)
# {% load statictags %}{% static 'pyconau2017/images/svgs/illustrations/' %}{{ illustration }}
@register.simple_tag(takes_context=False)
def illustration(name):
return staticfiles.static('pyconau2017/images/svgs/illustrations/') + name
@register.simple_tag(takes_context=True)
def speaker_photo(context, speaker, size):
''' Provides the speaker profile, or else fall back to libravatar or gravatar. '''
if speaker.photo:
thumbnailer = get_thumbnailer(speaker.photo)
thumbnail_options = {'crop': True, 'size': (size, size)}
thumbnail = thumbnailer.get_thumbnail(thumbnail_options)
return thumbnail.url
else:
email = speaker.user.email.encode("utf-8")
md5sum = hashlib.md5(email.strip().lower()).hexdigest()
fallback_image = ("https://2017.pycon-au.org/site_media/static"
"/pyconau23017/images/speaker-fallback-devil.jpg")
url = "https://secure.gravatar.com/avatar/%s?s=%d&d=%s" % (md5sum, size, fallback_image)
return url
@register.simple_tag()
def define(value):
return value
@register.simple_tag()
def presentation_bg_number(presentation, count):
return sum(ord(i) for i in presentation.title) % count
@register.filter()
def gst(amount):
two_places = Decimal(10) ** -2
return Decimal(amount / 11).quantize(two_places)
@register.simple_tag()
def conference_name():
return conference_models.Conference.objects.get(id=CONFERENCE_ID).title
@register.filter()
def trackname(room, day):
try:
track_name = room.track_set.get(day=day).name
except Track.DoesNotExist:
track_name = None
return track_name
@register.simple_tag()
def sponsor_thumbnail(sponsor_logo):
if sponsor_logo is not None:
if sponsor_logo.upload:
return sponsor_logo.upload.url
return ""

View file

@ -107,3 +107,29 @@ div.system-message p.system-message-title {
font-size: 12px;
padding: 5px 0 0 12px;
}
/**
* The CSS shown here will not be introduced in the Quickstart guide, but shows
* how you can use CSS to style your Element's container.
*/
.StripeElement {
background-color: white;
padding: 8px 12px;
border-radius: 4px;
border: 3px solid transparent;
box-shadow: 1px 3px 5px 1px #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
}
.StripeElement--focus {
box-shadow: 1px 3px 5px 1px #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

64
vendor/registrasion/.gitignore vendored Normal file
View file

@ -0,0 +1,64 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Grumble OSX Grumble
.DS_Store
*/.DS_Store

11
vendor/registrasion/.gitrepo vendored Normal file
View file

@ -0,0 +1,11 @@
; DO NOT EDIT (unless you know what you are doing)
;
; This subdirectory is a git "subrepo", and this file is maintained by the
; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme
;
[subrepo]
remote = git@gitlab.com:tchaypo/registrasion.git
branch = lca2018
commit = 3545a809e8e14014963c670709b6d0273c0e354a
parent = 19e4185cd9433c8f743c32dde8aee09455db3982
cmdver = 0.3.1

39
vendor/registrasion/CONTRIBUTING.rst vendored Normal file
View file

@ -0,0 +1,39 @@
Contributing to Registrasion
============================
I'm glad that you're interested in helping make Registrasion better! Thanks! This guide is meant to help make sure your contributions to the project fit in well.
Making a contribution
---------------------
This project makes use of GitHub issues to track pending work, so new features that need implementation can be found there. If you think a new feature needs to be added, raise an issue for discussion before submitting a Pull Request.
Code Style
----------
We use PEP8. Your code should pass checks by ``flake8`` with no errors or warnings before it will be merged.
We use `Google-style docstrings <http://sphinxcontrib-napoleon.readthedocs.org/en/latest/example_google.html>`_, primarily because they're far far more readable than ReST docstrings. New functions should have complete docstrings, so that new contributors have a chance of understanding how the API works.
Structure
---------
Django Models live in ``registrasion/models``; we separate our models out into separate files, because there's a lot of them. Models are grouped by logical functionality.
Actions that operate on Models live in ``registrasion/controllers``.
Testing
-------
Functionality that lives in ``regsistrasion/controllers`` was developed in a test-driven fashion, which is sensible, given it's where most of the business logic for registrasion lives. If you're changing behaviour of a controller, either submit a test with your pull request, or modify an existing test.
Documentation
-------------
Registrasion aims towards high-quality documentation, so that conference registration managers can understand how the system works, and so that webmasters working for conferences understand how the system fits together. Make sure that you have docstrings :)
The documentation is written in Australian English: *-ise* and not *-ize*, *-our* and not *-or*; *vegemite* and not *peanut butter*, etc etc etc.

View file

@ -1,8 +1,6 @@
Registrasion
============
Nick was here ...
**Registra** (tion for Sympo) **sion**. A conference registration app for Django,
letting conferences big and small sell tickets from within Symposion.

298
vendor/registrasion/design/design.md vendored Normal file
View file

@ -0,0 +1,298 @@
# Logic
## Definitions
- User has one 'active Cart' at a time. The Cart remains active until a paid Invoice is attached to it.
- A 'paid Cart' is a Cart with a paid Invoice attached to it, where the Invoice has not been voided.
- An unpaid Cart is 'reserved' if
- CURRENT_TIME - "Time last updated" <= max(reservation duration of Products in Cart),
- A Voucher was added and CURRENT_TIME - "Time last updated" < VOUCHER_RESERVATION_TIME (15 minutes?)
- An Item is 'reserved' if:
- it belongs to a reserved Cart
- it belongs to a paid Cart
- A Cart can have any number of Items added to it, subject to limits.
## Entering Vouchers
- Vouchers are attached to Carts
- A user can enter codes for as many different Vouchers as they like.
- A Voucher is added to the Cart if the number of paid or reserved Carts containing the Voucher is less than the "total available" for the voucher.
- A cart is invalid if it contains a voucher that has been overused
## Are products available?
- Availability is determined by the number of items we want to add to the cart: items_to_add
- If items_to_add + count(Product in their active and paid Carts) > "Limit per user" for the Product, the Product is "unavailable".
- If the Product belongs to an exhausted Ceiling, the Product is "unavailable".
- Otherwise, the product is available
## Displaying Products:
- If there is at least one mandatory EnablingCondition attached to the Product, display it only if all EnablingConditions are met
- If there is at least one EnablingCondition attached to the Product, display it only if at least one EnablingCondition is met
- If there are zero EnablingConditions attached to the Product, display it
- If the product is not available for items_to_add=0, mark it as "unavailable"
- If the Product is displayed and available, its price is the price for the Product, minus the greatest Discount available to this Cart and Product
- The product is displayed per the rendering characteristics of the Category it belongs to
## Displaying Categories
- If the Category contains only "unavailable" Products, mark it as "unavailable"
- If the Category contains no displayed Products, do not display the Category
- If the Category contains at least one EnablingCondition, display it only if at least one EnablingCondition is met
- If the Category contains no EnablingConditions, display it
## Exhausting Ceilings
- Exhaustion is determined by the number of items we want to add to the cart: items_to_add
- A ceiling is exhausted if:
- Its start date has not yet been reached
- Its end date has been exceeded
- items_to_add + sum(paid and reserved Items for each Product in the ceiling) > Total available
## Applying Discounts
- Discounts only apply to the current cart
- Discounts can be applied to multiple carts until the user has exhausted the quantity for each product attached to the discount.
- Only one discount discount can be applied to each single item. Discounts are applied as follows:
- All non-exhausted discounts for the product or its category are ordered by value
- The highest discount is applied for the lower of the quantity of the product in the cart, or the remaining quantity from this discount
- If the quantity remaining is non-zero, apply the next available discount
- Individual discount objects should not contain more than one DiscountForProduct for the same product
- Individual discount objects should not contain more than one DiscountForCategory for the same category
- Individual discount objects should not contain a discount for both a product and its category
## Adding Items to the Cart
- Products that are not displayed may not be added to a Cart
- The requested number of items must be available for those items to be added to a Cart
- If a different price applies to a Product when it is added to a cart, add at the new price, and display an alert to the user
- If a discount is used when adding a Product to the cart, add the discount as well
- Adding an item resets the "Time last updated" for the cart
- Each time carts have items added or removed, the revision number is updated
## Generating an invoice
- User can ask to 'check out' the active Cart. Doing so generates an Invoice. The invoice corresponds to a revision number of the cart.
- Checking out the active Cart resets the "Time last updated" for the cart.
- The invoice represents the current state of the cart.
- If the revision number for the cart is different to the cart's revision number for the invoice, the invoice is void.
- The invoice is void if
## Paying an invoice
- A payment can only be attached to an invoice if all of the items in it are available at the time payment is processed
### One-Shot
- Update the "Time last updated" for the cart based on the expected time it takes for a payment to complete
- Verify that all items are available, and if so:
- Proceed to make payment
- Apply payment record from amount received
### Authorization-based approach:
- Capture an authorization on the card
- Verify that all items are available, and if so:
- Apply payment record
- Take payment
# Registration workflow:
## User has not taken a guided registration yet:
User is shown two options:
1. Undertake guided registration ("for current user")
1. Purchase vouchers
## User has not purchased a ticket, and wishes to:
This gives the user a guided registration process.
1. Take list of categories, sorted by display order, and display the next lowest enabled & available category
1. Take user to category page
1. User can click "back" to go to previous screen, or "next" to go the next lowest enabled & available category
Once all categories have been seen:
1. Ask for badge information -- badge information is *not* the same as the invoicee.
1. User is taken to the "user has purchased a ticket" workflow
## User is buying vouchers
TODO: Consider separate workflow for purchasing ticket vouchers.
## User has completed a guided registration or purchased vouchers
1. Show list of products that are pending purchase.
1. Show list of categories + badge information, as well as 'checkout' button if the user has items in their current cart
## Category page
- User can enter a voucher at any time
- User is shown the list of products that have been paid for
- User has the option to add/remove products that are in the current cart
## Checkout
1. Ask for invoicing details (pre-fill from previous invoice?)
1. Ask for payment
# User Models
- Profile:
- User
- Has done guided registration?
- Badge
-
## Transaction Models
- Cart:
- User
- {Items}
- {Voucher}
- {DiscountItems}
- Time last updated
- Revision Number
- Active?
- Item
- Product
- Quantity
- DiscountItem
- Product
- Discount
- Quantity
- Invoice:
- Invoice number
- User
- Cart
- Cart Revision
- {Line Items}
- (Invoice Details)
- {Payments}
- Voided?
- LineItem
- Description
- Quantity
- Price
- Payment
- Time
- Amount
- Reference
## Inventory Model
- Product:
- Name
- Description
- Category
- Price
- Limit per user
- Reservation duration
- Display order
- {Ceilings}
- Voucher
- Description
- Code
- Total available
- Category?
- Name
- Description
- Display Order
- Rendering Style
## Product Modifiers
- Discount:
- Description
- {DiscountForProduct}
- {DiscountForCategory}
- Discount Types:
- TimeOrStockLimitDiscount:
* A discount that is available for a limited amount of time, e.g. Early Bird sales *
- Start date
- End date
- Total available
- VoucherDiscount:
* A discount that is available to a specific voucher *
- Voucher
- RoleDiscount
* A discount that is available to a specific role *
- Role
- IncludedProductDiscount:
* A discount that is available because another product has been purchased *
- {Parent Product}
- DiscountForProduct
- Product
- Amount
- Percentage
- Quantity
- DiscountForCategory
- Category
- Percentage
- Quantity
- EnablingCondition:
- Description
- Mandatory?
- {Products}
- {Categories}
- EnablingCondition Types:
- ProductEnablingCondition:
* Enabling because the user has purchased a specific product *
- {Products that enable}
- CategoryEnablingCondition:
* Enabling because the user has purchased a product in a specific category *
- {Categories that enable}
- VoucherEnablingCondition:
* Enabling because the user has entered a voucher code *
- Voucher
- RoleEnablingCondition:
* Enabling because the user has a specific role *
- Role
- TimeOrStockLimitEnablingCondition:
* Enabling because a time condition has been met, or a number of items underneath it have not been sold *
- Start date
- End date
- Total available

55
vendor/registrasion/design/goals.md vendored Normal file
View file

@ -0,0 +1,55 @@
# Registrasion
## What
A registration package that sits on top of the Symposion conference management system. It aims to be able to model complex events, such as those used by [Linux Australia events](http://lca2016.linux.org.au/register/info?_code=301).
## Planned features
### KEY:
- _(MODEL)_: these have model/controller functionality, and tests, and needs UI
- _(ADMIN)_: these have admin functionality
### Inventory
- Allow conferences to manage complex inventories of products, including tickets, t-shirts, dinner tickets, and accommodation _(MODEL)_ _(ADMIN)_
- Reports of available inventory and progressive sales for conference staff
- Restrict sales of products to specific classes of users
- Restrict sales of products based to users who've purchased specific products _(MODEL)_ _(ADMIN)_
- Restrict sales of products based on time/inventory limits _(MODEL)_ _(ADMIN)_
- Restrict sales of products to users with a voucher _(MODEL)_ _(ADMIN)_
### Tickets
- Sell multiple types of tickets, each with different included products _(MODEL)_ _(ADMIN)_
- Allow for early bird-style discounts _(MODEL)_ _(ADMIN)_
- Allow attendees to purchase products after initial registration is complete _(MODEL)_
- Offer included products if they have not yet been claimed _(MODEL)_
- Automatically offer free tickets to speakers and team
- Offer free tickets for sponsor attendees by voucher _(MODEL)_ _(ADMIN)_
### Vouchers
- Vouchers for arbitrary discounts off visible products _(MODEL)_ _(ADMIN)_
- Vouchers that enable secret products _(MODEL)_ _(ADMIN)_
### Invoicing
- Automatic invoicing including discount calculation _(MODEL)_
- Manual invoicing for arbitrary products by organisers _(MODEL)_
- Refunds
### Payments
- Allow multiple payment gateways (so that conferences are not locked into specific payment providers)
- Allow payment of registrations by unauthenticated users (allow business admins to pay for registrations)
- Allow payment of multiple registrations at once
### Attendee profiles
- Attendees can enter information to be shown on their badge/dietary requirements etc
- Profile can be changed until check-in, allowing for badge/company updates
### At the conference
- Badge generation, in batches, or on-demand during check-in
- Registration manifests for each attendee including purchased products
- Check-in process at registration desk allowing manifested items to be claimed
### Tooling
- Generate simple registration cases (ones that don't have complex inventory requirements)
- Generate complex registration cases from spreadsheets

View file

@ -24,6 +24,7 @@ Contents:
payments
for-zookeepr-users
views
templates
Indices and tables

78
vendor/registrasion/docs/templates.rst vendored Normal file
View file

@ -0,0 +1,78 @@
Basic Templates
===============
Registrasion provides basic templates for all of its views. This means that if new features come along, you won't need to do extra work just to enable them.
What is the point of this?
--------------------------
`registrasion` provides a bunch of django views that make the app tick. As new features get added, so will this package. By keeping this package up to date, you'll get a default template for each new view that gets added.
How does it work
----------------
For each template required by registrasion, `registrasion_templates` provides two templates. Say the template used by the view is called `view.html`. We provide:
* `view.html`, which is the template that is loaded directly -- this will be *very* modular, and will let you easily override things that you need to override in your own installations
* `view_.html`, which is the thing that lays everything out.
So you can either override `view_.html` if you're happy with the text and markup that `view.html` provides, or you can override `view.html` if you want to change the entire thing. Your choice!
Installation
------------
Ensure that `APP_DIRS` is switched on in your `settings`, like so:
```
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
}]
```
Overriding our defaults:
~~~~~~~~~~~~~~~~~~~~~~~~
* `registrasion/form.html` is used by these templates whenever a form needs to be rendered. The default implementation of this just calls ``{form}``, however, you may want to render your forms differently.
* `registrasion/base.html` extends `site_base.html`. Each `view_.html` template that we provide extends `registrasion/base.html`
Using the templates
-------------------
* All of the default templates provide the following blocks:
* `title`, which is added to the site's `<title>` tag
* `heading`, which is the page's heading
* `lede`, a paragraph that describes the page
* `content`, the body content for the page
* If you want that content to appear in your pages, you must include these blocks in your `registrasion/base.html`.
* `content` may include other blocks, so that you can override default text. Each `view.html` template will document the blocks that you can override.
CSS styling
-----------
The in-built templates do a small amount of layout and styling work, using bootstrap conventions. The following CSS classes are used:
* `panel panel-default`
* `panel panel-primary`
* `panel panel-info`
* `panel-heading`
* `panel-title`
* `panel-body`
* `panel-footer`
* `form-actions`
* `btn btn-default`
* `btn btn-primary`
* `btn btn-xs btn-default`
* `alert alert-info`
* `alert alert-warning`
* `list-group`
* `list-group-item`
* `well`
* `table`
* `table table-striped`

View file

@ -1,4 +0,0 @@
from registrasion.models.commerce import * # NOQA
from registrasion.models.conditions import * # NOQA
from registrasion.models.inventory import * # NOQA
from registrasion.models.people import * # NOQA

View file

@ -0,0 +1,392 @@
'''
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 django.db.utils import OperationalError
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))
## It's possible that this script will be run before the database has been populated
try:
Volunteers = Group.objects.filter(name='Conference volunteers').first().user_set.all()
Organisers = Group.objects.filter(name='Conference organisers').first().user_set.all()
except (OperationalError, AttributeError):
Volunteers = []
Organisers = []
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 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):

View file

@ -9,6 +9,8 @@ from django.db.models import Value
from .batch import BatchController
from operator import attrgetter
class AllProducts(object):
pass
@ -26,7 +28,7 @@ class CategoryController(object):
products, otherwise it'll do all. '''
# STOPGAP -- this needs to be elsewhere tbqh
from registrasion.controllers.product import ProductController
from .product import ProductController
if products is AllProducts:
products = inventory.Product.objects.all().select_related(
@ -38,7 +40,7 @@ class CategoryController(object):
products=products,
)
return set(i.category for i in available)
return sorted(set(i.category for i in available), key=attrgetter("order"))
@classmethod
@BatchController.memoise

View file

@ -152,16 +152,16 @@ class CategoryConditionController(IsMetByFilter, ConditionController):
product from a category invoking that item's condition in one of their
carts. '''
active = commerce.Cart.STATUS_ACTIVE
paid = commerce.Cart.STATUS_PAID
in_user_carts = Q(
enabling_category__product__productitem__cart__user=user
)
released = commerce.Cart.STATUS_RELEASED
in_released_carts = Q(
enabling_category__product__productitem__cart__status=released
enabling_category__product__productitem__cart__user=user,
enabling_category__product__productitem__cart__status=active
) | Q(
enabling_category__product__productitem__cart__user=user,
enabling_category__product__productitem__cart__status=paid
)
queryset = queryset.filter(in_user_carts)
queryset = queryset.exclude(in_released_carts)
return queryset

View file

@ -4,7 +4,7 @@ from django.db import transaction
from registrasion.models import commerce
from registrasion.controllers.for_id import ForId
from .for_id import ForId
class CreditNoteController(ForId, object):
@ -40,8 +40,8 @@ class CreditNoteController(ForId, object):
paid.
'''
# Circular Import
from registrasion.controllers.invoice import InvoiceController
# Local import to fix import cycles. Can we do better?
from .invoice import InvoiceController
inv = InvoiceController(invoice)
inv.validate_allowed_to_pay()
@ -65,8 +65,8 @@ class CreditNoteController(ForId, object):
a cancellation fee. Must be 0 <= percentage <= 100.
'''
# Circular Import
from registrasion.controllers.invoice import InvoiceController
# Local import to fix import cycles. Can we do better?
from .invoice import InvoiceController
assert(percentage >= 0 and percentage <= 100)

View file

@ -45,6 +45,28 @@ class FlagController(object):
else:
all_conditions = []
all_conditions = conditions.FlagBase.objects.filter(
id__in=set(i.id for i in all_conditions)
).select_subclasses()
# Prefetch all of the products and categories (Saves a LOT of queries)
all_conditions = all_conditions.prefetch_related(
"products", "categories", "products__category",
)
# Now pre-select all of the products attached to those categories
all_categories = set(
cat for condition in all_conditions
for cat in condition.categories.all()
)
all_category_ids = (i.id for i in all_categories)
all_category_products = inventory.Product.objects.filter(
category__in=all_category_ids
).select_related("category")
products_by_category_ = itertools.groupby(all_category_products, lambda prod: prod.category)
products_by_category = dict((k.id, list(v)) for (k, v) in products_by_category_)
# All disable-if-false conditions on a product need to be met
do_not_disable = defaultdict(lambda: True)
# At least one enable-if-true condition on a product must be met
@ -64,17 +86,19 @@ class FlagController(object):
# Get all products covered by this condition, and the products
# from the categories covered by this condition
ids = [product.id for product in products]
# TODO: This is re-evaluated a lot.
all_products = inventory.Product.objects.filter(id__in=ids)
cond = (
Q(flagbase_set=condition) |
Q(category__in=condition.categories.all())
condition_products = condition.products.all()
category_products = (
product for cat in condition.categories.all() for product in products_by_category[cat.id]
)
all_products = all_products.filter(cond)
all_products = all_products.select_related("category")
all_products = itertools.chain(
condition_products, category_products
)
all_products = set(all_products)
# Filter out the products from this condition that
# are not part of this query.
all_products = set(i for i in all_products if i in products)
if quantities:
consumed = sum(quantities[i] for i in all_products)

View file

@ -10,9 +10,9 @@ from registrasion.models import commerce
from registrasion.models import conditions
from registrasion.models import people
from registrasion.controllers.cart import CartController
from registrasion.controllers.credit_note import CreditNoteController
from registrasion.controllers.for_id import ForId
from .cart import CartController
from .credit_note import CreditNoteController
from .for_id import ForId
class InvoiceController(ForId, object):

View file

@ -1,6 +1,6 @@
from registrasion.controllers.product import ProductController
from registrasion.models import commerce
from registrasion.models import inventory
from .controllers.product import ProductController
from .models import commerce
from .models import inventory
from django import forms
from django.db.models import Q
@ -31,12 +31,13 @@ class ApplyCreditNoteForm(forms.Form):
"user_email": users[invoice["user_id"]].email,
})
key = lambda inv: (0 - (inv["user_id"] == self.user.id), inv["id"]) # noqa
invoices_annotated.sort(key=key)
template = ('Invoice %(id)d - user: %(user_email)s (%(user_id)d) '
'- $%(value)d')
template = (
'Invoice %(id)d - user: %(user_email)s (%(user_id)d) '
'- $%(value)d'
)
return [
(invoice["id"], template % invoice)
for invoice in invoices_annotated
@ -94,6 +95,7 @@ def ProductsForm(category, products):
cat.RENDER_TYPE_QUANTITY: _QuantityBoxProductsForm,
cat.RENDER_TYPE_RADIO: _RadioButtonProductsForm,
cat.RENDER_TYPE_ITEM_QUANTITY: _ItemQuantityProductsForm,
cat.RENDER_TYPE_CHECKBOX: _CheckboxProductsForm,
}
# Produce a subclass of _ProductsForm which we can alter the base_fields on
@ -252,6 +254,39 @@ class _RadioButtonProductsForm(_ProductsForm):
self.add_error(self.FIELD, error)
class _CheckboxProductsForm(_ProductsForm):
''' Products entry form that allows users to say yes or no
to desired products. Basically, it's a quantity form, but the quantity
is either zero or one.'''
@classmethod
def set_fields(cls, category, products):
for product in products:
if product.price:
label='%s -- $%s' % (product.name, product.price)
else:
label='%s' % (product.name)
field = forms.BooleanField(
label=label,
required=False,
)
cls.base_fields[cls.field_name(product)] = field
@classmethod
def initial_data(cls, product_quantities):
initial = {}
for product, quantity in product_quantities:
initial[cls.field_name(product)] = bool(quantity)
return initial
def product_quantities(self):
for name, value in self.cleaned_data.items():
if name.startswith(self.PRODUCT_PREFIX):
product_id = int(name[len(self.PRODUCT_PREFIX):])
yield (product_id, int(value))
class _ItemQuantityProductsForm(_ProductsForm):
''' Products entry form that allows users to select a product type, and
enter a quantity of that product. This version _only_ allows a single
@ -449,7 +484,6 @@ class InvoicesWithProductAndStatusForm(forms.Form):
product = [int(i) for i in product]
super(InvoicesWithProductAndStatusForm, self).__init__(*a, **k)
print(status)
qs = commerce.Invoice.objects.filter(
status=status or commerce.Invoice.STATUS_UNPAID,
@ -491,3 +525,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

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2017-05-26 16:24
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0005_auto_20160905_0945'),
]
operations = [
migrations.AlterField(
model_name='category',
name='render_type',
field=models.IntegerField(choices=[(1, 'Radio button'), (2, 'Quantity boxes'), (3, 'Product selector and quantity box'), (4, 'Checkbox button')], help_text='The registration form will render this category in this style.', verbose_name='Render type'),
),
]

View file

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-29 13:31
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0006_auto_20170702_2233'),
('registrasion', '0006_auto_20170526_1624'),
]
operations = [
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-30 08:43
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registrasion', '0007_merge_20170929_2331'),
]
operations = [
migrations.AlterField(
model_name='attendee',
name='guided_categories_complete',
field=models.ManyToManyField(blank=True, to='registrasion.Category'),
),
]

View file

@ -0,0 +1,4 @@
from .commerce import * # NOQA
from .conditions import * # NOQA
from .inventory import * # NOQA
from .people import * # NOQA

View file

@ -324,7 +324,6 @@ class CreditNote(PaymentBase):
elif hasattr(self, 'creditnoterefund'):
reference = self.creditnoterefund.reference
print(reference)
return "Refunded with reference: %s" % reference
raise ValueError("This should never happen.")

View file

@ -42,6 +42,8 @@ class Category(models.Model):
have a lot of options, from which the user is not going to select
all of the options.
``RENDER_TYPE_CHECKBOX`` shows a checkbox beside each product.
limit_per_user (Optional[int]): This restricts the number of items
from this Category that each attendee may claim. This extends
across multiple Invoices.
@ -63,11 +65,13 @@ class Category(models.Model):
RENDER_TYPE_RADIO = 1
RENDER_TYPE_QUANTITY = 2
RENDER_TYPE_ITEM_QUANTITY = 3
RENDER_TYPE_CHECKBOX = 4
CATEGORY_RENDER_TYPES = [
(RENDER_TYPE_RADIO, _("Radio button")),
(RENDER_TYPE_QUANTITY, _("Quantity boxes")),
(RENDER_TYPE_ITEM_QUANTITY, _("Product selector and quantity box")),
(RENDER_TYPE_CHECKBOX, _("Checkbox button")),
]
name = models.CharField(

View file

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

View file

@ -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):
@ -177,12 +185,13 @@ class Links(Report):
return []
def rows(self, content_type):
print(self._links)
for url, link_text in self._links:
yield [
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
@ -299,9 +308,10 @@ class ReportView(object):
response = HttpResponse(content_type='text/csv')
writer = csv.writer(response)
writer.writerow(report.headings())
encode = lambda i: i.encode("utf8") if isinstance(i, unicode) else i # NOQA
writer.writerow(list(encode(i) for i in report.headings()))
for row in report.rows():
writer.writerow(row)
writer.writerow(list(encode(i) for i in row))
return response

View file

@ -1,4 +1,4 @@
from registrasion.reporting import forms
from . import forms
import collections
import datetime
@ -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
@ -24,11 +26,13 @@ from registrasion import views
from symposion.schedule import models as schedule_models
from registrasion.reporting.reports import get_all_reports
from registrasion.reporting.reports import Links
from registrasion.reporting.reports import ListReport
from registrasion.reporting.reports import QuerysetReport
from registrasion.reporting.reports import report_view
from .reports import get_all_reports
from .reports import Links
from .reports import ListReport
from .reports import QuerysetReport
from .reports import report_view
import bleach
def CURRENCY():
@ -95,8 +99,6 @@ def items_sold():
total_quantity=Sum("quantity"),
)
print(line_items)
headings = ["Description", "Quantity", "Price", "Total"]
data = []
@ -242,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
@ -312,6 +391,55 @@ def discount_status(request, form):
return ListReport("Usage by item", headings, data)
@report_view("Product Line Items By Date & Customer", form_type=forms.ProductAndCategoryForm)
def product_line_items(request, form):
''' Shows each product line item from invoices, including their date and
purchashing customer. '''
products = form.cleaned_data["product"]
categories = form.cleaned_data["category"]
invoices = commerce.Invoice.objects.filter(
(
Q(lineitem__product__in=products) |
Q(lineitem__product__category__in=categories)
),
status=commerce.Invoice.STATUS_PAID,
).select_related(
"cart",
"user",
"user__attendee",
"user__attendee__attendeeprofilebase"
).order_by("issue_time").distinct()
headings = [
'Invoice', 'Invoice Date', 'Attendee', 'Qty', 'Product', 'Status'
]
data = []
for invoice in invoices:
for item in invoice.cart.productitem_set.all():
if item.product in products or item.product.category in categories:
output = []
output.append(invoice.id)
output.append(invoice.issue_time.strftime('%Y-%m-%d %H:%M:%S'))
output.append(
invoice.user.attendee.attendeeprofilebase.attendee_name()
)
output.append(item.quantity)
output.append(item.product)
cart = invoice.cart
if cart.status == commerce.Cart.STATUS_PAID:
output.append('PAID')
elif cart.status == commerce.Cart.STATUS_ACTIVE:
output.append('UNPAID')
elif cart.status == commerce.Cart.STATUS_RELEASED:
output.append('REFUNDED')
data.append(output)
return ListReport("Line Items", headings, data)
@report_view("Paid invoices by date", form_type=forms.ProductAndCategoryForm)
def paid_invoices_by_date(request, form):
''' Shows the number of paid invoices containing given products or
@ -339,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),
@ -353,8 +481,8 @@ def paid_invoices_by_date(request, form):
)
by_date[date] += 1
data = [(date, count) for date, count in sorted(by_date.items())]
data = [(date.strftime("%Y-%m-%d"), count) for date, count in data]
data = [(date_, count) for date_, count in sorted(by_date.items())]
data = [(date_.strftime("%Y-%m-%d"), count) for date_, count in data]
return ListReport(
"Paid Invoices By Date",
@ -417,20 +545,19 @@ def attendee(request, form, user_id=None):
if user_id is None:
return attendee_list(request)
print(user_id)
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"])
@ -444,11 +571,20 @@ 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):
try:
value = bleach.clean(value)
except TypeError:
value = "Bad value for %s" % field.name
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))
@ -470,53 +606,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
@ -633,6 +781,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, )
@ -667,7 +816,7 @@ def attendee_data(request, form, user_id=None):
return None
else:
def display_field(value):
return value
return bleach.clean(str(value))
status_count = lambda status: Case(When( # noqa
attendee__user__cart__status=status,
@ -714,7 +863,10 @@ 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
try:
return bleach.clean(attr)
except TypeError:
return "Bad value found for %s" % attr
headings = ["User ID", "Name", "Email", "Product", "Item Status"]
headings.extend(field_names)
@ -846,9 +998,10 @@ def manifest(request, form):
headings = ["User ID", "Name", "Paid", "Unpaid", "Refunded"]
def format_items(item_list):
strings = []
for item in item_list:
strings.append('%d x %s' % (item.quantity, str(item.product)))
strings = [
'%d x %s' % (item.quantity, str(item.product))
for item in item_list
]
return ", \n".join(strings)
output = []

View file

@ -0,0 +1,5 @@
{% extends "registrasion/amend_registration_.html" %}
{% comment %}
Blocks that you can override:
{% endcomment %}

View file

@ -0,0 +1,81 @@
{% extends "registrasion/base.html" %}
{% load registrasion_tags %}
{% block title %}Amend registration{% endblock %}
{% block heading %}Amend registration{% endblock %}
{% block content %}
<dl>
<dt>Attendee name</dt>
<dd>{{ user.attendee.attendeeprofilebase.attendee_name }}</dd>
<dt>Attendee ID</dt>
<dd>{{ user.id }}</dd>
</dl>
<h2>Item summary</h2>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Paid Items</h3>
</div>
<div class="panel-body">
<div class="alert alert-warning">
You cannot remove paid items from someone's registration. You must first
cancel the invoice that added those items. You will need to re-add the items
from that invoice for the user to have them available again.
</div>
</div>
{% include "registrasion/snippets/items_list.html" with items=paid ul_class="list-group" li_class="list-group-item" %}
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Cancelled Items</h3>
</div>
{% if cancelled %}
{% include "registrasion/snippets/items_list.html" with items=cancelled ul_class="list-group" li_class="list-group-item" %}
{% else %}
<div class="panel-body">No cancelled items.</div>
{% endif %}
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Amend pending items</h3>
</div>
<form method="POST">
<div class="panel-body">
{% csrf_token %}
{% include "registrasion/form.html" with form=form %}
</div>
<div class="panel-footer">
<input class="btn btn-primary" type="submit">
<!-- todo: disable the checkout button if the form changes. -->
<a class="btn btn-default" href="{% url "checkout" user.id %}">Check out cart and view invoice</a>
</div>
</form>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Apply voucher</h3>
</div>
<form method="POST">
<div class="panel-body">
{% csrf_token %}
{% include "registrasion/form.html" with form=voucher_form %}
</div>
<div class="panel-footer">
<input class="btn btn-primary" type="submit">
</div>
</form>
</div>
{% endblock %}

View file

@ -0,0 +1,5 @@
{% extends "registrasion/badges_.html" %}
{% comment %}
Blocks that you can override:
{% endcomment %}

Some files were not shown because too many files have changed in this diff Show more