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-stipe | ||||
|     # Registrasion-stripe | ||||
|     "pinax.stripe", | ||||
|     "django_countries", | ||||
|     "registripe", | ||||
| 
 | ||||
|     #registrasion-desk | ||||
|     "regidesk", | ||||
| 
 | ||||
|     # admin - required by registrasion ?? | ||||
|     "nested_admin", | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,7 +26,7 @@ urlpatterns = [ | |||
|     url(r'^tickets/payments/', include('registripe.urls')), | ||||
|     url(r'^tickets/', include('registrasion.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'^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 | ||||
| 
 | ||||
| # 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.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.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 | ||||
| 
 | ||||
| app_name='regidesk' | ||||
| 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 | ||||
| from regidesk import models | ||||
| import base64 | ||||
| import logging | ||||
| from datetime import datetime | ||||
| 
 | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.core.mail import EmailMultiAlternatives | ||||
| from django.conf import settings | ||||
| 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.models import F, Q | ||||
| from django.db.models import Count, Max, Sum | ||||
| from django.http import Http404 | ||||
| from django.http import HttpResponse | ||||
| from django.http import HttpResponse, HttpResponseBadRequest | ||||
| 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 regidesk import forms | ||||
| from regidesk.models import BoardingPass, BoardingPassTemplate, CheckIn | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| AttendeeProfile = util.get_object_from_name(settings.ATTENDEE_PROFILE_MODEL) | ||||
| 
 | ||||
| def _staff_only(user): | ||||
|     ''' Returns true if the 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 | ||||
| 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", | ||||
|     author="James Polley", | ||||
|     author_email="jamezpolley@gmail", | ||||
|     version=registripe.__version__, | ||||
|     version=regidesk.__version__, | ||||
|     description="Registration desk functionality for registrasion", | ||||
|     url="http://gitlab.com/lca2018/registrasion-desk/", | ||||
|     packages=find_packages(), | ||||
|  |  | |||
|  | @ -1,2 +1,3 @@ | |||
| vendor/registrasion | ||||
| vendor/registripe | ||||
| vendor/regidesk | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 James Polley
						James Polley