Add Venueless integration to Regidesk

Create Venueless login token as part of checkin to allow attendee to
join an online event from the dashboard.
This commit is contained in:
Joel Addison 2021-01-09 01:26:13 +10:00
parent ef148ea482
commit 7c77001d5e
11 changed files with 142 additions and 18 deletions

View file

@ -570,3 +570,10 @@ SPEAKERS_DINNER_ADULT = SpeakersDinnerCat.create(
# "Infant must be seated in an adult's lap. " # "Infant must be seated in an adult's lap. "
# "No food or beverage service.", # "No food or beverage service.",
# timedelta(hours=1)) # timedelta(hours=1))
# Venueless integration
VENUELESS_URL = os.environ.get('VENUELESS_URL', None)
VENUELESS_AUDIENCE = os.environ.get('VENUELESS_AUDIENCE', "venueless")
VENUELESS_TOKEN_ISSUER = os.environ.get('VENUELESS_TOKEN_ISSUER', "any")
VENUELESS_SECRET = os.environ.get('VENUELESS_SECRET', SECRET_KEY)

View file

@ -41,6 +41,18 @@
</div> </div>
</div> </div>
{% else %} {% else %}
{% flag "venueless_dashboard" %}
<div class="row">
<div class="col-12 mb-3">
<h3>Join the Conference</h3>
<p>The conference is now open. Please join us to watch talks and interact with the other delegates.</p>
<div>
<a class="btn btn-lg btn-primary" role="button" href="{% venueless_login_url %}">Launch Conference</a>
</div>
</div>
</div>
{% endflag %}
<div class="row"> <div class="row">
<div class="col-md-6 mb-3 mb-md-0"> <div class="col-md-6 mb-3 mb-md-0">
<h3>Attendee Profile</h3> <h3>Attendee Profile</h3>

View file

@ -8,6 +8,7 @@ from easy_thumbnails.files import get_thumbnailer
from registrasion.templatetags import registrasion_tags from registrasion.templatetags import registrasion_tags
from symposion.conference import models as conference_models from symposion.conference import models as conference_models
from symposion.schedule.models import Track from symposion.schedule.models import Track
from regidesk.models import CheckIn
CONFERENCE_ID = settings.CONFERENCE_ID CONFERENCE_ID = settings.CONFERENCE_ID
GST_RATE = settings.GST_RATE GST_RATE = settings.GST_RATE
@ -158,3 +159,10 @@ def ticket_type(context):
# Default to product type # Default to product type
return ticket_type return ticket_type
@register.simple_tag(takes_context=True)
def venueless_login_url(context):
user=context.request.user
checkin = CheckIn.objects.get_or_create(user=user)[0]
return f'{settings.VENUELESS_URL}/#token={checkin.venueless_token}'

View file

@ -36,6 +36,7 @@ django-ical==1.7.1
# Registrasion reqs # Registrasion reqs
django-nested-admin==3.3.2 django-nested-admin==3.3.2
CairoSVG==2.4.2 CairoSVG==2.4.2
PyJWT==2.0.0
# Registripe # Registripe
django-countries>=6.1.3 django-countries>=6.1.3

View file

@ -14,7 +14,7 @@ def create_lca2018_template(apps, schema_editor):
BoardingPassTemplate = apps.get_model("regidesk", "BoardingPassTemplate") BoardingPassTemplate = apps.get_model("regidesk", "BoardingPassTemplate")
body = ("This is the plain text version of your boarding pass for " 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 " "linux.conf.au 2021.\r\n\r\nWhen you check in at LCA, you'll "
"need to show the QR code you can download from " "need to show the QR code you can download from "
"{{ qrcode_url }}, or quote registration code: {{ code }} ") "{{ qrcode_url }}, or quote registration code: {{ code }} ")
html = ("<html>\r\n <body>\r\n <p>This is your boarding " html = ("<html>\r\n <body>\r\n <p>This is your boarding "
@ -23,9 +23,9 @@ def create_lca2018_template(apps, schema_editor):
"phone or on a print out.</p>\r\n " "phone or on a print out.</p>\r\n "
"<p><img src=\"data:image/png;base64,{{ qrcode }}\" /></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>") " <p>Backup Code: {{ code }}</p>\r\n </body>\r\n</html>")
template = BoardingPassTemplate(label="LCA2018", template = BoardingPassTemplate(label="LCA2021",
from_address="team@lca2018.org", from_address="contact@lca2021.linux.org.au",
subject="Your boarding pass for LCA2018, " subject="Your boarding pass for LCA2021, "
"{{ user.attendee.attendeeprofilebase.attendeeprofile.name }}", "{{ user.attendee.attendeeprofilebase.attendeeprofile.name }}",
body=body, body=body,
html_body=html) html_body=html)

View file

@ -0,0 +1,28 @@
# Generated by Django 2.2.17 on 2021-01-08 23:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('regidesk', '0004_auto_20180121_1140'),
]
operations = [
migrations.AddField(
model_name='checkin',
name='venueless_traits',
field=models.TextField(blank=True, max_length=256, null=True),
),
migrations.AddField(
model_name='checkin',
name='venueless_user_id',
field=models.TextField(blank=True, max_length=40, null=True),
),
migrations.AddField(
model_name='checkin',
name='_venueless_token',
field=models.TextField(blank=True, null=True),
),
]

View file

@ -1,12 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import base64 import base64
from datetime import datetime import datetime
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
import jwt
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils import timezone from django.utils import timezone
from django.conf import settings
from django.db import models from django.db import models
from django.db.models import Q, F from django.db.models import Q, F
from django.db.models import Case, When, Value from django.db.models import Case, When, Value
@ -79,6 +81,10 @@ class CheckIn(models.Model):
needs_review = models.BooleanField(default=False) needs_review = models.BooleanField(default=False)
review_text = models.TextField(blank=True) review_text = models.TextField(blank=True)
venueless_user_id = models.TextField(max_length=40, null=True, blank=True)
venueless_traits = models.TextField(max_length=256, null=True, blank=True)
_venueless_token = models.TextField(null=True, blank=True)
class Meta: class Meta:
permissions = ( permissions = (
("view_checkin_details", "Can view the details of other user's checkins"), ("view_checkin_details", "Can view the details of other user's checkins"),
@ -143,3 +149,41 @@ class CheckIn(models.Model):
self.save() self.save()
return self._checkin_code_png return self._checkin_code_png
@property
def venueless_token(self):
"""Returns the Venueless JWT token for this checkin's code."""
updated = False
if not self.venueless_user_id:
self.venueless_user_id = self.checkin_code
updated = True
if not self.venueless_traits:
self.venueless_traits = ".".join(("attendee",))
updated = True
if not self._venueless_token:
self._venueless_token = self._generate_venueless_token()
updated = True
if updated:
self.save()
return self._venueless_token
def _generate_venueless_token(self):
""" Generate token for Venueless login """
issued_at = datetime.datetime.utcnow()
expiry = settings.LCA_END + datetime.timedelta(days=1)
payload = {
"iss": settings.VENUELESS_TOKEN_ISSUER,
"aud": settings.VENUELESS_AUDIENCE,
"iat": issued_at,
"exp": expiry,
"uid": self.venueless_user_id,
"traits": self.venueless_traits.split(','),
}
token = jwt.encode(payload, settings.VENUELESS_SECRET, algorithm="HS256")
return token

View file

@ -6,9 +6,9 @@
{% items_purchased as purchased %} {% items_purchased as purchased %}
{% items_pending as pending %} {% items_pending as pending %}
{% items_purchased 1 as ticket %} {% items_purchased 1 as ticket %}
{% total_items_purchased 2 as penguin_dinner_count %} <!-- {% total_items_purchased 2 as penguin_dinner_count %} -->
{% total_items_purchased 3 as speakers_dinner_count %} <!-- {% total_items_purchased 3 as speakers_dinner_count %} -->
{% total_items_purchased 4 as pdns_count %} <!-- {% total_items_purchased 4 as pdns_count %} -->
{% ticket_type as ticket_type %} {% ticket_type as ticket_type %}
{% block header_title %} {% block header_title %}

View file

@ -6,9 +6,9 @@
{% items_purchased as purchased %} {% items_purchased as purchased %}
{% items_pending as pending %} {% items_pending as pending %}
{% items_purchased 1 as ticket %} {% items_purchased 1 as ticket %}
{% total_items_purchased 2 as penguin_dinner_count %} <!-- {% total_items_purchased 2 as penguin_dinner_count %} -->
{% total_items_purchased 3 as speakers_dinner_count %} <!-- {% total_items_purchased 3 as speakers_dinner_count %} -->
{% total_items_purchased 4 as pdns_count %} <!-- {% total_items_purchased 4 as pdns_count %} -->
{% ticket_type as ticket_type %} {% ticket_type as ticket_type %}
{% block body_class %}{{ block.super }} review-results{% endblock %} {% block body_class %}{{ block.super }} review-results{% endblock %}

View file

@ -6,9 +6,9 @@
{% items_purchased as purchased %} {% items_purchased as purchased %}
{% items_pending as pending %} {% items_pending as pending %}
{% items_purchased 1 as ticket %} {% items_purchased 1 as ticket %}
{% total_items_purchased 2 as penguin_dinner_count %} <!-- {% total_items_purchased 2 as penguin_dinner_count %} -->
{% total_items_purchased 3 as speakers_dinner_count %} <!-- {% total_items_purchased 3 as speakers_dinner_count %} -->
{% total_items_purchased 4 as pdns_count %} <!-- {% total_items_purchased 4 as pdns_count %} -->
{% ticket_type as ticket_type %} {% ticket_type as ticket_type %}

View file

@ -9,10 +9,10 @@
{% items_purchased as purchased %} {% items_purchased as purchased %}
{% items_pending as pending %} {% items_pending as pending %}
{% items_purchased 1 as ticket %} {% items_purchased 1 as ticket %}
{% items_purchased 6 as shirts %} <!-- {% items_purchased 6 as shirts %} -->
{% total_items_purchased 3 as penguin_dinner_count %} <!-- {% total_items_purchased 3 as penguin_dinner_count %} -->
{% total_items_purchased 4 as speakers_dinner_count %} <!-- {% total_items_purchased 4 as speakers_dinner_count %} -->
{% total_items_purchased 5 as pdns_count %} <!-- {% total_items_purchased 5 as pdns_count %} -->
{% ticket_type as ticket_type %} {% ticket_type as ticket_type %}
<a type="button" class="btn btn-outline-primary" href="{% url 'regidesk:check_in_scanner' %}">Return to scanning page</a> <a type="button" class="btn btn-outline-primary" href="{% url 'regidesk:check_in_scanner' %}">Return to scanning page</a>
@ -36,6 +36,7 @@
<dt class="col-sm-3">Free Text 2</dt> <dt class="col-sm-3">Free Text 2</dt>
<dd class="col-sm-9">{{ user.attendee.attendeeprofilebase.attendeeprofile.free_text_2 }}</dd> <dd class="col-sm-9">{{ user.attendee.attendeeprofilebase.attendeeprofile.free_text_2 }}</dd>
{% comment "Not needed for LCA2021 online" %}
<dt class="col-sm-3">Penguin Dinner Tickets</dt> <dt class="col-sm-3">Penguin Dinner Tickets</dt>
<dd class="col-sm-9">{{ penguin_dinner_count }}</dd> <dd class="col-sm-9">{{ penguin_dinner_count }}</dd>
@ -47,11 +48,13 @@
<dt class="col-sm-3">Over 18 years</dt> <dt class="col-sm-3">Over 18 years</dt>
<dd class="col-sm-9">{% if user.attendee.attendeeprofilebase.attendeeprofile.of_legal_age %}Yes{% else %}<strong class="red">NO</strong>{% endif %}</dd> <dd class="col-sm-9">{% if user.attendee.attendeeprofilebase.attendeeprofile.of_legal_age %}Yes{% else %}<strong class="red">NO</strong>{% endif %}</dd>
{% endcomment %}
<dt class="col-sm-3">Username</dt> <dt class="col-sm-3">Username</dt>
<dd class="col-sm-9">{{ user.username }}</dd> <dd class="col-sm-9">{{ user.username }}</dd>
</dl> </dl>
{% comment "Not needed for LCA2021 online" %}
<h4>Shirts ordered</h4> <h4>Shirts ordered</h4>
<table class="table card-text"> <table class="table card-text">
{% for shirt in shirts%} {% for shirt in shirts%}
@ -61,6 +64,23 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endcomment %}
</div>
</div>
<div class="card my-3">
<div class="card-header">Venueless</div>
<div class="card-body">
<dl class="card-text row">
<dt class="col-sm-3">User Id</dt>
<dd class="col-sm-9">{{ check_in.venueless_user_id }}</dd>
<dt class="col-sm-3">Traits</dt>
<dd class="col-sm-9">{{ check_in.venueless_traits }}</dd>
<dt class="col-sm-3">Token</dt>
<dd class="col-sm-9">{{ check_in.venueless_token|truncatechars:20 }}</dd>
</dl>
</div> </div>
</div> </div>
@ -108,6 +128,7 @@
</div> </div>
</div> </div>
{% comment "Not needed for LCA2021 online" %}
<div class="card {% if check_in.schwag_given %}border-success{% else %}border-danger{% endif %} my-3"> <div class="card {% if check_in.schwag_given %}border-success{% else %}border-danger{% endif %} my-3">
<div class="card-header {% if check_in.schwag_given %}text-white bg-success{% endif %}">Schwag</div> <div class="card-header {% if check_in.schwag_given %}text-white bg-success{% endif %}">Schwag</div>
<div class="card-body"> <div class="card-body">
@ -121,6 +142,7 @@
</form> </form>
</div> </div>
</div> </div>
{% endcomment %}
<div class="card my-3"> <div class="card my-3">
<div class="card-header">Log Exception</div> <div class="card-header">Log Exception</div>
@ -133,6 +155,7 @@
</div> </div>
</div> </div>
{% comment "Not needed for LCA2021 online" %}
<div class="card my-3 {% if check_in.checked_in_bool and check_in.schwag_given %}border-success{% elif check_in.checked_in_bool or check_in.schwag_given %}card-warning{% else %}card-danger{% endif %}"> <div class="card my-3 {% if check_in.checked_in_bool and check_in.schwag_given %}border-success{% elif check_in.checked_in_bool or check_in.schwag_given %}card-warning{% else %}card-danger{% endif %}">
<div class="card-header {% if check_in.checked_in_bool and check_in.schwag_given %}text-white bg-success{% elif check_in.checked_in_bool or check_in.schwag_given %}bg-warning{% endif %}">Bulk actions</div> <div class="card-header {% if check_in.checked_in_bool and check_in.schwag_given %}text-white bg-success{% elif check_in.checked_in_bool or check_in.schwag_given %}bg-warning{% endif %}">Bulk actions</div>
<div class="card-body"> <div class="card-body">
@ -153,6 +176,7 @@
</form> </form>
</div> </div>
</div> </div>
{% endcomment %}
<a type="button" class="btn btn-outline-primary" href="{% url 'regidesk:check_in_scanner' %}">Return to scanning page</a> <a type="button" class="btn btn-outline-primary" href="{% url 'regidesk:check_in_scanner' %}">Return to scanning page</a>