Create regidesk app

Shows summary of all attendees with a paid ticket, including
boarding_pass status.

Currently, regidesk allows staff with the requisite permission the
ability to view the checkin status of attendees, and email the user
their boarding pass email.

Included is a view for the user to retrieve their own QR code (in case
they got the plain-text version of the email, they can use this to
download an image to their phone for faster checkin)
This commit is contained in:
James Polley 2017-12-29 11:38:56 +11:00
parent 44cdd088be
commit e726ff21a8
16 changed files with 746 additions and 11 deletions

View file

@ -209,11 +209,14 @@ INSTALLED_APPS = [
# Registrasion # Registrasion
"registrasion", "registrasion",
# Registrasion-stipe # Registrasion-stripe
"pinax.stripe", "pinax.stripe",
"django_countries", "django_countries",
"registripe", "registripe",
#registrasion-desk
"regidesk",
# admin - required by registrasion ?? # admin - required by registrasion ??
"nested_admin", "nested_admin",

View file

@ -26,7 +26,7 @@ urlpatterns = [
url(r'^tickets/payments/', include('registripe.urls')), url(r'^tickets/payments/', include('registripe.urls')),
url(r'^tickets/', include('registrasion.urls')), url(r'^tickets/', include('registrasion.urls')),
url(r'^nested_admin/', include('nested_admin.urls')), url(r'^nested_admin/', include('nested_admin.urls')),
url(r'^checkin/', include('regidesk.urls')),
url(r'^pages/', include('django.contrib.flatpages.urls')), url(r'^pages/', include('django.contrib.flatpages.urls')),
url(r'^dashboard/', RedirectView.as_view(url='/')), url(r'^dashboard/', RedirectView.as_view(url='/')),

1
vendor/regidesk/MANIFEST.in vendored Normal file
View file

@ -0,0 +1 @@
recursive-include regidesk/templates *

View file

@ -1,3 +1,26 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. # Register your models here.
from regidesk.models import BoardingPassTemplate, BoardingPass, CheckIn
admin.site.register(
BoardingPassTemplate,
list_display=['label','from_address','subject']
)
admin.site.register( BoardingPass,
list_display=['to_address','created','sent'],
search_fields=['to_address'],
filter_fields=['created','sent'],
readonly_fields=['created','sent',
'template', 'to_address', 'from_address',
'subject', 'body','html_body' ]
)
admin.site.register(
CheckIn,
list_display=['user','seen','checked_in','checkin_code'],
search_fields=['user','checkin_code'],
filter_fields=['seen','checked_in'],
readonly_fields=['user','seen','checked_in','checkin_code']
)

View file

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.8 on 2018-01-06 00:19
from __future__ import unicode_literals
import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
def create_lca2018_template(apps, schema_editor):
BoardingPassTemplate = apps.get_model("regidesk", "BoardingPassTemplate")
body = ("This is the plain text version of your boarding pass for "
"linux.conf.au 2018.\r\n\r\nWhen you check in at LCA, you'll "
"need to show the QR code you can download from "
"{{ qrcode_url }}, or quote registration code: {{ code }} ")
html = ("<html>\r\n <body>\r\n <p>This is your boarding "
"pass</p>\r\n <p>A copy of the QR Code is required "
"for check in, please bring this email on either your "
"phone or on a print out.</p>\r\n "
"<p><img src=\"data:image/png;base64,{{ qrcode }}\" /></p>\r\n"
" <p>Backup Code: {{ code }}</p>\r\n </body>\r\n</html>")
template = BoardingPassTemplate(label="LCA2018",
from_address="team@lca2018.org",
subject="Your boarding pass for LCA2018, "
"{{ user.attendee.attendeeprofilebase.attendeeprofile.name }}",
body=body,
html_body=html)
template.save()
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BoardingPass',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('sent', models.DateTimeField(null=True, verbose_name='Sent')),
('to_address', models.EmailField(max_length=254, verbose_name='To address')),
('from_address', models.EmailField(max_length=254, verbose_name='From address')),
('subject', models.CharField(max_length=255, verbose_name='Subject')),
('body', models.TextField(verbose_name='Body')),
('html_body', models.TextField(null=True, verbose_name='HTML Body')),
],
options={
'permissions': (('view_boarding_pass', 'Can view sent boarding passes'), ('send_boarding_pass', 'Can send boarding passes')),
},
),
migrations.CreateModel(
name='BoardingPassTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=100, verbose_name='Label')),
('from_address', models.EmailField(max_length=254, verbose_name='From address')),
('subject', models.CharField(max_length=100, verbose_name='Subject')),
('body', models.TextField(verbose_name='Body')),
('html_body', models.TextField(null=True, verbose_name='HTML Body')),
],
options={
'verbose_name': 'Boarding Pass template',
'verbose_name_plural': 'Boarding Pass templates',
},
),
migrations.CreateModel(
name='CheckIn',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('seen', models.DateTimeField(blank=True, null=True)),
('checked_in', models.DateTimeField(blank=True, null=True)),
('checkin_code', models.CharField(db_index=True, max_length=6, unique=True)),
('_checkin_code_png', models.TextField(blank=True, max_length=512, null=True)),
('boardingpass', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='regidesk.BoardingPass')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'permissions': (('view_checkin_details', "Can view the details of other user's checkins"),),
},
),
migrations.AddField(
model_name='boardingpass',
name='template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='regidesk.BoardingPassTemplate', verbose_name='Template'),
),
migrations.RunPython(
code=create_lca2018_template,
),
]

View file

View file

@ -1,4 +1,110 @@
from __future__ import unicode_literals # -*- coding: utf-8 -*-
import base64
from datetime import datetime
from decimal import Decimal
from io import BytesIO
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Q, F
from django.db.models import Case, When, Value
from django.db.models import Count
from django.db.models.signals import post_save
from django.contrib.auth.models import User
import pyqrcode
from symposion import constants
from symposion.text_parser import parse
from registrasion.models import commerce from registrasion.models import commerce
from registrasion.util import generate_access_code as generate_code
class BoardingPassTemplate(models.Model):
label = models.CharField(max_length=100, verbose_name="Label")
from_address = models.EmailField(verbose_name="From address")
subject = models.CharField(max_length=100, verbose_name="Subject")
body = models.TextField(verbose_name="Body")
html_body = models.TextField(verbose_name="HTML Body",null=True)
class Meta:
verbose_name = ("Boarding Pass template")
verbose_name_plural = ("Boarding Pass templates")
class BoardingPass(models.Model):
template = models.ForeignKey(BoardingPassTemplate, null=True, blank=True,
on_delete=models.SET_NULL, verbose_name="Template")
created = models.DateTimeField(auto_now_add=True, verbose_name="Created")
sent = models.DateTimeField(null=True, verbose_name="Sent")
to_address = models.EmailField(verbose_name="To address")
from_address = models.EmailField(verbose_name="From address")
subject = models.CharField(max_length=255, verbose_name="Subject")
body = models.TextField(verbose_name="Body")
html_body = models.TextField(verbose_name="HTML Body", null=True)
class Meta:
permissions = (
("view_boarding_pass", "Can view sent boarding passes"),
("send_boarding_pass", "Can send boarding passes"),
)
def __unicode__(self):
return self.checkin.attendee.attendeeprofilebase.attendeeprofile.name + ' ' + self.timestamp.strftime('%Y-%m-%d %H:%M:%S')
@property
def email_args(self):
return (self.subject, self.body, self.from_address, self.user.email)
class CheckIn(models.Model):
user = models.OneToOneField(User)
boardingpass = models.OneToOneField(BoardingPass, null=True,
blank=True, on_delete=models.SET_NULL)
seen = models.DateTimeField(null=True,blank=True)
checked_in = models.DateTimeField(null=True,blank=True)
checkin_code = models.CharField(
max_length=6,
unique=True,
db_index=True,
)
_checkin_code_png=models.TextField(max_length=512,null=True,blank=True)
class Meta:
permissions = (
("view_checkin_details", "Can view the details of other user's checkins"),
)
def save(self, *a, **k):
while not self.checkin_code:
checkin_code = generate_code()
if CheckIn.objects.filter(checkin_code=checkin_code).count() == 0:
self.checkin_code = checkin_code
return super(CheckIn, self).save(*a, **k)
@property
def code(self):
return self.checkin_code
@property
def qrcode(self):
"""Returns the QR Code for this checkin's code.
If this is the first time the QR code has been generated, cache it on the object.
If a code has already been cached, serve that.
Returns the raw PNG blob, unless b64=True, in which case the return value
is the base64encoded PNG blob."""
if not self.code:
return None
if not self._checkin_code_png:
qrcode = pyqrcode.create(self.code)
qr_io = BytesIO()
qrcode.png(qr_io, scale=6)
qr_io.seek(0)
self._checkin_code_png = base64.b64encode(qr_io.read()).decode('UTF-8')
self.save()
return self._checkin_code_png

View file

@ -0,0 +1,9 @@
<b>Body</b> may include the following variables which will be substituted in the email with a value
specific to each proposal:
<ul>
<li><code>{% templatetag openvariable %} user {% templatetag closevariable %}</code> e.g. {{ sample.user }}
<li><code>{% templatetag openvariable %} qrcode {% templatetag closevariable %}</code> e.g. <code>&lt;img src=&quot;data:image/png;base64,{% templatetag openvariable %} qrcode {% templatetag closevariable %}&quot; /&gt;</code> produces <img src="data:image/png;base64,{{ sample.qrcode }}" />
<li><code>{% templatetag openvariable %} qrcode_url {% templatetag closevariable %}</code> e.g. {{ sample.qrcode_url }}
<li><code>{% templatetag openvariable %} code {% templatetag closevariable %}</code> e.g. {{ sample.code }}
<li><code>{% templatetag openvariable %} user.attendee.ticket_type {% templatetag closevariable %}</code> e.g. {{ sample.user.attendee.ticket_type }}
</ul>

View file

@ -0,0 +1,46 @@
{% extends "site_base.html" %}
{% load staticfiles %}
{% load i18n %}
{% block body_class %}reviews{% endblock %}
{% block body_outer %}
<div class="l-content-page">
<div class="l-content-page--richtext">
<div class="rich-text">
<div class="row">
<div class="col-md-10">
{% block body %}
{% endblock %}
</div>
</div>
</div></div></div>
{% endblock %}
{% block extra_script %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs/jszip-2.5.0/dt-1.10.16/b-1.4.2/b-colvis-1.4.2/b-flash-1.4.2/b-html5-1.4.2/b-print-1.4.2/cr-1.4.1/fc-3.2.3/fh-3.1.3/r-2.2.0/rg-1.0.2/datatables.min.css"/>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/v/bs/jszip-2.5.0/dt-1.10.16/b-1.4.2/b-colvis-1.4.2/b-html5-1.4.2/b-print-1.4.2/cr-1.4.1/fc-3.2.3/fh-3.1.3/r-2.2.0/rg-1.0.2/datatables.min.js"></script>
<script type="text/javascript">
$("table.table-data").dataTable({
"dom": "<'row'<'col-md-3'l><'col-md-3'B><'col-md-4'f>r>t<'row'<'col-md-3'i><'col-md-5'p>>",
"stateSave": true,
"lengthMenu": [[10, 50, 100, -1], [10, 50, 100, "All"]],
"pageLength": 100,
"colReorder": true,
"buttons": [ {
extend: 'collection',
text: 'Export',
buttons: ["copy", "csv", "print"]
},
{ extend: 'collection',
text: 'Columns',
buttons: [
{ extend: 'columnsToggle',
columns: '.toggle' },
]
}]});
</script>
{% endblock %}

View file

@ -0,0 +1,179 @@
{% extends "regidesk/base.html" %}
{% load i18n %}
{% load registrasion_tags %}
{% load lca2018_tags %}
{% items_purchased as purchased %}
{% items_pending as pending %}
{% items_purchased 1 as ticket %}
{% total_items_purchased 2 as penguin_dinner_count %}
{% total_items_purchased 3 as speakers_dinner_count %}
{% total_items_purchased 4 as pdns_count %}
{% ticket_type as ticket_type %}
{% block body_class %}{{ block.super }} review-results{% endblock %}
{% block extra_style %}
{{ block.super }}
<style type="text/css">
.table-striped tbody tr.selected td {
background-color: #F7F4E6;
}
</style>
{% endblock %}
{% block body %}
<h1>Boarding Pass Overview</h1>
<form class="form-horizontal" method="post" action="{% url "regidesk:boarding_prepare" %}">
{% csrf_token %}
<p>
Select one or more attendees (<span class="action-counter">0</span> currently selected)
<br/>
then pick an email template
<select name="template">
<option value="">[blank]</option>
{% for template in templates %}
<option value="{{ template.pk }}">{{ template.label }}</option>
{% endfor %}
</select>
<br/>
<button id="next-button" type="submit" class="btn btn-primary" disabled>Next <i class="fa fa-chevron-right"></i></button>
</p>
<table class="table table-striped table-bordered dataTable">
<thead>
<th><input type="checkbox" id="action-toggle"></th>
<th class="toggle">#</th>
<th>Attendee Name</th>
<th>Ticket Type</th>
<th class="toggle">Attendee email</th>
<th class="toggle">Checkin Code</th>
<th>Notified?</th>
</thead>
<tbody>
{% for attendee in attendees %}
<tr>
<td><input class="action-select" type="checkbox" name="_selected_action" value="{{ attendee.pk }}"></td>
<td>{{ attendee.id }}</td>
<td>{{ attendee.attendeeprofilebase.attendeeprofile.name }}</td>
<td>{{ attendee.ticket_type }}</td>
<td>{{ attendee.user.email }}</td>
<td>{{ attendee.user.checkin.code }}</td>
<td>
{% if attendee.user.checkin %}
{% if attendee.user.checkin.boardingpass %}
Boarding pass sent<br/>
{% else %}
Checkin Created
{% endif %}
{% else %}
Pending
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% endblock %}
{% block extra_script %}
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs/jszip-2.5.0/dt-1.10.16/b-1.4.2/b-colvis-1.4.2/b-flash-1.4.2/b-html5-1.4.2/b-print-1.4.2/cr-1.4.1/fc-3.2.3/fh-3.1.3/r-2.2.0/rg-1.0.2/datatables.min.css"/>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/v/bs/jszip-2.5.0/dt-1.10.16/b-1.4.2/b-colvis-1.4.2/b-html5-1.4.2/b-print-1.4.2/cr-1.4.1/fc-3.2.3/fh-3.1.3/r-2.2.0/rg-1.0.2/datatables.min.js"></script>
<script type="text/javascript">
(function($) {
$.fn.actions = function(opts) {
var options = $.extend({}, $.fn.actions.defaults, opts);
var actionCheckboxes = $(this);
checker = function(checked) {
$(actionCheckboxes).prop("checked", checked)
.parent().parent().toggleClass(options.selectedClass, checked);
}
updateCounter = function() {
var sel = $(actionCheckboxes).filter(":checked").length;
$(options.counterContainer).html(sel);
$(options.allToggle).prop("checked", function() {
if (sel == actionCheckboxes.length) {
value = true;
} else {
value = false;
}
return value;
});
if (sel == 0) {
$("#next-button").prop("disabled", true);
} else {
$("#next-button").prop("disabled", false);
}
}
// Check state of checkboxes and reinit state if needed
$(this).filter(":checked").each(function(i) {
$(this).parent().parent().toggleClass(options.selectedClass);
updateCounter();
});
$(options.allToggle).click(function() {
checker($(this).prop("checked"));
updateCounter();
});
lastChecked = null;
$(actionCheckboxes).click(function(event) {
if (!event) { var event = window.event; }
var target = event.target ? event.target : event.srcElement;
if (lastChecked && $.data(lastChecked) != $.data(target) && event.shiftKey == true) {
var inrange = false;
$(lastChecked).prop("checked", target.checked)
.parent().parent().toggleClass(options.selectedClass, target.checked);
$(actionCheckboxes).each(function() {
if ($.data(this) == $.data(lastChecked) || $.data(this) == $.data(target)) {
inrange = (inrange) ? false : true;
}
if (inrange) {
$(this).prop("checked", target.checked)
.parent().parent().toggleClass(options.selectedClass, target.checked);
}
});
}
$(target).parent().parent().toggleClass(options.selectedClass, target.checked);
lastChecked = target;
updateCounter();
});
}
/* Setup plugin defaults */
$.fn.actions.defaults = {
counterContainer: "span.action-counter",
allToggle: "#action-toggle",
selectedClass: "selected"
}
})($);
$(function() {
$("tr input.action-select").actions();
});
$('.dataTable').dataTable({
"dom": "<'row'<'col-md-3'l><'col-md-3'B><'col-md-4'f>r>t<'row'<'col-md-3'i><'col-md-5'p>>",
"stateSave": true,
"lengthMenu": [[10, 50, 100, -1], [10, 50, 100, "All"]],
"drawCallback": function( settings ) {
$("tr input.action-select").actions();
},
"pageLength": 100,
"colReorder": true,
"buttons": [ {
extend: 'collection',
text: 'Export',
buttons: ["copy", "csv", "print"]
},
{ extend: 'collection',
text: 'Columns',
buttons: [
{ extend: 'columnsToggle',
columns: '.toggle' },
]
}]});
</script>
{% endblock %}

View file

@ -0,0 +1,74 @@
{% extends "regidesk/base.html" %}
{% load i18n %}
{% load registrasion_tags %}
{% load lca2018_tags %}
{% items_purchased as purchased %}
{% items_pending as pending %}
{% items_purchased 1 as ticket %}
{% total_items_purchased 2 as penguin_dinner_count %}
{% total_items_purchased 3 as speakers_dinner_count %}
{% total_items_purchased 4 as pdns_count %}
{% ticket_type as ticket_type %}
{% block body %}
<h1>BoardingPass Preparation</h1>
<div class="row">
<div class="col-md-4">
<h2>Attendees</h2>
<table class="table table-striped table-compact">
{% for attendee in attendees %}
{% with profile=attendee.attendeeprofilebase.attendeeprofile %}
<tr>
<td>
<strong>{{ profile.name }}</strong> ({{ attendee.user.email }})<br />
{{ attendee.ticket_type }}<br/>
{{ profile.company }}<br/>
{{ profile.free_text_1 }}<br/>
{{ profile.free_text_2 }}<br/>
</td>
</tr>
{% endwith %}
{% endfor %}
</table>
</div>
<div class="col-md-6">
<h2>Email</h2>
<form class="form-horizontal" method="post" action="{% url "regidesk:boarding_send" %}">
{% csrf_token %}
<label>From Address</label>
<input type="text" name="from_address" class="span5 label-required" value="{{ template.from_address }}" />
<br/>
<label>Subject</label>
<input type="text" name="subject" class="span5" value="{{ subject }}" />
<br/>
<label>Template</label>
<a href=" {% url 'admin:regidesk_boardingpasstemplate_change' template.id %}">
<input type="text" readonly class="form-control-plaintext span5" value="{{ template.label }}">
</a>
<div class="panel panel-default">
<ul class="nav nav-tabs panel-heading" id="templates" role="tablist">
<li class="nav-item"><a class="nav-link active" id="plain_template" data-toggle="tab" href="#plain" role="tab" aria-selected="true">Plaintext</a></li>
<li class="nav-item"><a class="nav-link" id="html_template" data-toggle="tab" href="#html" role="tab" aria-selected="false">HTML</a></li>
</ul>
<div class="tab-content panel-body" id="templatesContent">
<div class="tab-pane active monospace-text" id="plain" role="tabpanel" aria-labelledby="plain_template">{{ rendered_template.plain }}</div>
<div class="tab-pane fade show" id="html" role="tabpanel" aria-labelledby="html_template">{{ rendered_template.html }}</div>
</div>
</div>
<input type="hidden" name="notification_template" value="{{ template.pk }}" />
<input type="hidden" name="attendees" value="{{ attendees }}" />
{% include "regidesk/_bp_prepare_help.html" %}
<button type="submit" class="btn btn-primary">Send {{ attendees|length }} Email{{ attendees|length|pluralize }}</button>
<a class="btn" href="{% url "regidesk:boarding_overview" %}">Cancel</a>
</form>
</div>
</form>
{% endblock %}

View file

@ -2,5 +2,11 @@ from django.conf.urls import url
from regidesk import views from regidesk import views
app_name='regidesk'
urlpatterns = [ urlpatterns = [
url(r"^([A-Z0-9]{6}$)", views.boarding_overview, name="checkin_detail"),
url(r"^([A-Z0-9]{6}).png$", views.checkin_png, name="checkin_png"),
url(r"^overview/([a-z]+)?$", views.boarding_overview, name="boarding_overview"),
url(r"^prepare_passes/", views.boarding_prepare, name="boarding_prepare"),
url(r"^send_passes/", views.boarding_send, name="boarding_send")
] ]

View file

@ -1,20 +1,207 @@
from regidesk import forms import base64
from regidesk import models import logging
from datetime import datetime
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import permission_required, user_passes_test, login_required
from django.db import transaction from django.db import transaction
from django.db.models import F, Q
from django.db.models import Count, Max, Sum
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.template import Template, Context
from django.urls import reverse
from registrasion.models import commerce from registrasion import util
from registrasion.models import commerce, people
from symposion.conference.models import Conference from symposion.conference.models import Conference
from regidesk import forms
from regidesk.models import BoardingPass, BoardingPassTemplate, CheckIn
AttendeeProfile = util.get_object_from_name(settings.ATTENDEE_PROFILE_MODEL)
def _staff_only(user): def _staff_only(user):
''' Returns true if the user is staff. ''' ''' Returns true if the user is staff. '''
return user.is_staff return user.is_staff
@permission_required("regidesk.view_boarding_pass")
def boarding_overview(request, boarding_state="pending"):
tickets = commerce.LineItem.objects.select_related(
"invoice","invoice__user__attendee","product__category"
).filter(
invoice__status=commerce.Invoice.STATUS_PAID,
product__category=settings.TICKET_PRODUCT_CATEGORY,
price__gte=0
)
ticketholders = { ticket.invoice.user: ticket.product.name for ticket in tickets }
attendees = people.Attendee.objects.select_related(
"attendeeprofilebase",
"attendeeprofilebase__attendeeprofile",
"user",
"user__checkin"
).filter(user__in=ticketholders)
profiles = AttendeeProfile.objects.filter(
attendee__in=attendees
).select_related(
"attendee", "attendee__user",
)
profiles_by_attendee = dict((i.attendee, i) for i in profiles)
bp_templates = BoardingPassTemplate.objects.all()
ctx = {
"boarding_state": boarding_state,
"attendees": attendees,
"profiles": profiles_by_attendee,
"templates": bp_templates,
}
return render(request, "regidesk/boardingpass_overview.html", ctx)
@login_required
def checkin_png(request, checkin_code):
checkin = CheckIn.objects.get(checkin_code=checkin_code)
if not checkin:
raise Http404()
if not request.user.has_perm("regidesk.view_checkin_details"):
if request.user != checkin.user:
raise Http404()
response = HttpResponse()
response["Content-Type"] = "image/png"
response["Content-Disposition"] = 'inline; filename="qrcode.png"'
qrcode = base64.b64decode(checkin.qrcode)
response.write(qrcode)
return response
@permission_required("regidesk.send_boarding_pass")
def boarding_prepare(request):
attendee_pks = []
try:
for pk in request.POST.getlist("_selected_action"):
attendee_pks.append(int(pk))
except ValueError:
return HttpResponseBadRequest()
attendees = people.Attendee.objects.filter(pk__in=attendee_pks)
attendees = attendees.select_related(
"user", "attendeeprofilebase", "attendeeprofilebase__attendeeprofile")
sample_checkin = CheckIn.objects.get_or_create(user=attendees[0].user)[0]
rendered_template = {}
sample_ctx = {}
bp_template_pk = request.POST.get("template", "")
if bp_template_pk:
bp_template = BoardingPassTemplate.objects.get(pk=bp_template_pk)
sample_ctx = {
"user": sample_checkin.user,
"boardingpass": sample_checkin.boardingpass,
"code": sample_checkin.code,
"qrcode": sample_checkin.qrcode,
"qrcode_url": request.build_absolute_uri(
reverse("regidesk:checkin_png", args=[sample_checkin.code])),
}
ctx = Context(sample_ctx)
subject = Template(bp_template.subject).render(ctx)
rendered_template['plain'] = Template(bp_template.body).render(ctx)
rendered_template['html'] = Template(bp_template.html_body).render(ctx)
else:
bp_template = None
subject = None
ctx = {
"attendees": attendees,
"template": bp_template,
"attendee_pks": attendee_pks,
"rendered_template": rendered_template,
"subject": subject,
"sample": sample_ctx,
}
request.session.set_expiry=(300)
request.session['boarding_attendees'] = attendee_pks
request.session['template'] = bp_template.pk
response = render(request, "regidesk/boardingpass_prepare.html", ctx)
return response
@permission_required("regidesk.send_boarding_pass")
def boarding_send(request):
attendees = people.Attendee.objects.filter(pk__in=request.session['boarding_attendees'])
attendees = attendees.select_related(
"user", "attendeeprofilebase", "attendeeprofilebase__attendeeprofile")
logging.debug(attendees)
template_pk = request.session['template']
template = BoardingPassTemplate.objects.get(pk=template_pk)
for attendee in attendees:
user = attendee.user
checkin = CheckIn.objects.get_or_create(user=user)
ctx = {
"user": user,
"checkin": user.checkin,
"code": user.checkin.code,
"qrcode": user.checkin.qrcode,
"qrcode_url": request.build_absolute_uri(
reverse("regidesk:checkin_png", args=[user.checkin.code])),
}
ctx = Context(ctx)
subject = Template(template.subject).render(ctx)
body = Template(template.body).render(ctx)
if template.html_body:
html_body = Template(template.html_body).render(ctx)
else:
html_body = None
bpass = BoardingPass(template=template, to_address=user.email,
from_address=template.from_address,
subject=subject, body=body,
html_body=html_body
)
bpass.save()
if user.checkin.boardingpass:
user.checkin.boardingpass.delete()
user.checkin.boardingpass = bpass
user.checkin.save()
with transaction.atomic():
msg = EmailMultiAlternatives(
bpass.subject,
bpass.body,
bpass.from_address,
[bpass.to_address,],
)
if bpass.html_body:
msg.attach_alternative(bpass.html_body, "text/html")
msg.send()
bpass.sent = datetime.now()
bpass.save()
messages.success(request, "Sent boarding pass to %s" % attendee)
request.session['boarding_attendees'].remove(attendee.pk)
return redirect("regidesk:boarding_overview")

View file

@ -1,3 +1,4 @@
django-countries>=4.0 django-countries>=4.0
requests>=2.11.1 requests>=2.11.1
pypng
pyqrcode

View file

@ -18,7 +18,7 @@ setup(
name="registrasion-desk", name="registrasion-desk",
author="James Polley", author="James Polley",
author_email="jamezpolley@gmail", author_email="jamezpolley@gmail",
version=registripe.__version__, version=regidesk.__version__,
description="Registration desk functionality for registrasion", description="Registration desk functionality for registrasion",
url="http://gitlab.com/lca2018/registrasion-desk/", url="http://gitlab.com/lca2018/registrasion-desk/",
packages=find_packages(), packages=find_packages(),

View file

@ -1,2 +1,3 @@
vendor/registrasion vendor/registrasion
vendor/registripe vendor/registripe
vendor/regidesk