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:
parent
44cdd088be
commit
e726ff21a8
16 changed files with 746 additions and 11 deletions
|
@ -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",
|
||||||
|
|
||||||
|
|
|
@ -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
1
vendor/regidesk/MANIFEST.in
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
recursive-include regidesk/templates *
|
23
vendor/regidesk/regidesk/admin.py
vendored
23
vendor/regidesk/regidesk/admin.py
vendored
|
@ -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']
|
||||||
|
)
|
||||||
|
|
99
vendor/regidesk/regidesk/migrations/0001_initial.py
vendored
Normal file
99
vendor/regidesk/regidesk/migrations/0001_initial.py
vendored
Normal 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,
|
||||||
|
),
|
||||||
|
]
|
0
vendor/regidesk/regidesk/migrations/__init__.py
vendored
Normal file
0
vendor/regidesk/regidesk/migrations/__init__.py
vendored
Normal file
108
vendor/regidesk/regidesk/models.py
vendored
108
vendor/regidesk/regidesk/models.py
vendored
|
@ -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
|
||||||
|
|
9
vendor/regidesk/regidesk/templates/regidesk/_bp_prepare_help.html
vendored
Normal file
9
vendor/regidesk/regidesk/templates/regidesk/_bp_prepare_help.html
vendored
Normal 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><img src="data:image/png;base64,{% templatetag openvariable %} qrcode {% templatetag closevariable %}" /></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>
|
46
vendor/regidesk/regidesk/templates/regidesk/base.html
vendored
Normal file
46
vendor/regidesk/regidesk/templates/regidesk/base.html
vendored
Normal 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 %}
|
179
vendor/regidesk/regidesk/templates/regidesk/boardingpass_overview.html
vendored
Normal file
179
vendor/regidesk/regidesk/templates/regidesk/boardingpass_overview.html
vendored
Normal 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 %}
|
74
vendor/regidesk/regidesk/templates/regidesk/boardingpass_prepare.html
vendored
Normal file
74
vendor/regidesk/regidesk/templates/regidesk/boardingpass_prepare.html
vendored
Normal 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 %}
|
6
vendor/regidesk/regidesk/urls.py
vendored
6
vendor/regidesk/regidesk/urls.py
vendored
|
@ -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")
|
||||||
]
|
]
|
||||||
|
|
199
vendor/regidesk/regidesk/views.py
vendored
199
vendor/regidesk/regidesk/views.py
vendored
|
@ -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")
|
||||||
|
|
3
vendor/regidesk/requirements.txt
vendored
3
vendor/regidesk/requirements.txt
vendored
|
@ -1,3 +1,4 @@
|
||||||
django-countries>=4.0
|
django-countries>=4.0
|
||||||
requests>=2.11.1
|
requests>=2.11.1
|
||||||
|
pypng
|
||||||
|
pyqrcode
|
||||||
|
|
2
vendor/regidesk/setup.py
vendored
2
vendor/regidesk/setup.py
vendored
|
@ -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(),
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
vendor/registrasion
|
vendor/registrasion
|
||||||
vendor/registripe
|
vendor/registripe
|
||||||
|
vendor/regidesk
|
||||||
|
|
Loading…
Reference in a new issue