Free and open source ticket system written in python

Merge branch 'fbl-integration' into main

authored by

A.Ottr and committed by
GitHub
5edd527a b322cd1f

+554 -479
+2 -2
.github/workflows/docker-image.yml
··· 3 3 on: 4 4 push: 5 5 branches: 6 - - "main" 6 + - "fbl-integration" 7 7 - "staging" 8 8 pull_request: 9 - branches: ["main"] 9 + branches: ["fbl-integration"] 10 10 11 11 env: 12 12 REGISTRY: ghcr.io
fbl_integration/__init__.py

This is a binary file and will not be displayed.

+13
fbl_integration/admin.py
··· 1 + from django.contrib import admin 2 + from .models import FblAccount 3 + 4 + @admin.register(FblAccount) 5 + class FblAccountAdmin(admin.ModelAdmin): 6 + list_display = ["system_username", "username", "badge_number", "status", "tags_secured", "created_at"] 7 + search_fields = ["badge_number", "username"] 8 + list_filter = ["status"] 9 + readonly_fields = ["badge_number", "account_id", "username", "status", "tags_secured", "created_at"] 10 + 11 + def system_username(self, x): 12 + return x.user.username 13 + system_username.short_description = 'System Username'
+6
fbl_integration/apps.py
··· 1 + from django.apps import AppConfig 2 + 3 + 4 + class FblIntegrationConfig(AppConfig): 5 + default_auto_field = "django.db.models.BigAutoField" 6 + name = "fbl_integration"
+68
fbl_integration/forms.py
··· 1 + from typing import Any 2 + from django import forms 3 + from django.conf import settings 4 + from django.utils.translation import gettext_lazy as _ 5 + from datetime import datetime 6 + 7 + class FblAuthForm(forms.Form): 8 + badge_number = forms.CharField(required=True) 9 + dob = forms.CharField(required=True) 10 + 11 + def clean(self) -> dict[str, Any]: 12 + cleaned_data = super(FblAuthForm, self).clean() 13 + badge_number = cleaned_data.get("badge_number") 14 + dob = cleaned_data.get("dob") 15 + 16 + if not badge_number: 17 + raise forms.ValidationError( 18 + _("Badge number is required") 19 + ) 20 + 21 + try: 22 + int(badge_number) 23 + except ValueError: 24 + raise forms.ValidationError( 25 + _("Badge number must be a number") 26 + ) 27 + 28 + if not dob: 29 + raise forms.ValidationError( 30 + _("Date of birth is required") 31 + ) 32 + try: 33 + dob = datetime.strptime(dob, "%d/%m/%Y").strftime("%Y-%m-%d") 34 + except ValueError: 35 + raise forms.ValidationError( 36 + _("Date of birth must be in the format DD/MM/YYYY") 37 + ) 38 + cleaned_data["dob"] = dob 39 + 40 + return cleaned_data 41 + 42 + class FblAuthCodeForm(FblAuthForm): 43 + validation_code = forms.CharField(required=True) 44 + 45 + def clean(self) -> dict[str, Any]: 46 + cleaned_data = super(FblAuthCodeForm, self).clean() 47 + validation_code = cleaned_data.get("validation_code") 48 + 49 + if not validation_code: 50 + raise forms.ValidationError( 51 + _("The validation code is required. Please check your emails.") 52 + ) 53 + 54 + return cleaned_data 55 + 56 + class RegistrationCompletionForm(forms.Form): 57 + email = forms.EmailField(required=True) 58 + 59 + def clean(self) -> dict[str, Any]: 60 + cleaned_data = super(RegistrationCompletionForm, self).clean() 61 + email = cleaned_data.get("email") 62 + 63 + if not email: 64 + raise forms.ValidationError( 65 + _("A Mail is required for notifications.") 66 + ) 67 + 68 + return cleaned_data
+44
fbl_integration/migrations/0001_initial.py
··· 1 + # Generated by Django 5.0.3 on 2024-04-06 18:26 2 + 3 + import django.db.models.deletion 4 + from django.conf import settings 5 + from django.db import migrations, models 6 + 7 + 8 + class Migration(migrations.Migration): 9 + 10 + initial = True 11 + 12 + dependencies = [ 13 + migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 + ] 15 + 16 + operations = [ 17 + migrations.CreateModel( 18 + name="FblAccount", 19 + fields=[ 20 + ( 21 + "id", 22 + models.BigAutoField( 23 + auto_created=True, 24 + primary_key=True, 25 + serialize=False, 26 + verbose_name="ID", 27 + ), 28 + ), 29 + ("badge_number", models.IntegerField()), 30 + ("account_id", models.CharField(max_length=255)), 31 + ("username", models.CharField(max_length=255)), 32 + ("tags_secured", models.CharField(max_length=255)), 33 + ("created_at", models.DateTimeField(auto_now_add=True)), 34 + ("status", models.CharField(max_length=64)), 35 + ( 36 + "user", 37 + models.OneToOneField( 38 + on_delete=django.db.models.deletion.CASCADE, 39 + to=settings.AUTH_USER_MODEL, 40 + ), 41 + ), 42 + ], 43 + ), 44 + ]
fbl_integration/migrations/__init__.py

This is a binary file and will not be displayed.

+35
fbl_integration/models.py
··· 1 + from django.db import models 2 + from core.models import PawUser 3 + 4 + class FblAccount(models.Model): 5 + user = models.OneToOneField(PawUser, on_delete=models.CASCADE) 6 + badge_number = models.IntegerField() 7 + account_id = models.CharField(max_length=255) 8 + username = models.CharField(max_length=255) 9 + tags_secured = models.CharField(max_length=255) 10 + created_at = models.DateTimeField(auto_now_add=True) 11 + status = models.CharField(max_length=64) 12 + 13 + def tags_list(self): 14 + return self.tags_secured.split(',') 15 + 16 + @classmethod 17 + def create_user(cls, username) -> PawUser: 18 + if not PawUser.objects.filter(username=username).exists(): 19 + user = PawUser.objects.create(username=username) 20 + else: 21 + counter = 1 22 + while True: 23 + new_username = f"{username}_{counter}" 24 + if not PawUser.objects.filter(username=new_username).exists(): 25 + user = PawUser.objects.create(username=new_username) 26 + break 27 + counter += 1 28 + 29 + user.set_unusable_password() 30 + user.save() 31 + return user 32 + 33 + 34 + def __str__(self): 35 + return self.user.username
+43
fbl_integration/templates/complete_registration.html
··· 1 + {% extends 'base.html' %} 2 + {% block content %} 3 + {% load i18n %} 4 + <div class="self-center w-full max-w-xl mx-auto"> 5 + <div class="flex items-center"> 6 + <h1 class="text-3xl font-bold p-2">{% trans 'FurryBlacklight Login' %}</h1> 7 + </div> 8 + <div class="bg-base-200 rounded p-8"> 9 + <ul class="steps w-full mb-6"> 10 + <li class="step step-accent">{% trans 'Authenticate' %}</li> 11 + <li class="step step-accent">{% trans 'Confirm Code' %}</li> 12 + <li class="step step-accent">{% trans 'Account Completion' %}</li> 13 + <li class="step step-success">{% trans 'Done' %}</li> 14 + </ul> 15 + 16 + <div role="alert" class="alert alert-info mb-4"> 17 + {% trans 'Please set up a mail address. This will be used for notifications.' %} 18 + </div> 19 + 20 + <form method="post" action=""> 21 + {% csrf_token %} 22 + 23 + {% if form.non_field_errors %} 24 + <div role="alert" class="alert alert-error mb-4"> 25 + <svg xmlns="http://www.w3.org/2000/svg" class="hidden sm:block stroke-current shrink-0 h-6 w-6" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10l4 4m0 -4l-4 4" /><path d="M12 3c7.2 0 9 1.8 9 9s-1.8 9 -9 9s-9 -1.8 -9 -9s1.8 -9 9 -9z" /></svg> 26 + <span>{{ form.non_field_errors }}</span> 27 + </div> 28 + {% endif %} 29 + 30 + <div class="form-control w-full"> 31 + <label class="label"> 32 + <span class="text-base label-text" for="{{ form.email.id_for_label }}">{% trans 'Mail Address' %}</span> 33 + </label> 34 + <input type="email" name="email" class="w-full input input-bordered" /> 35 + </div> 36 + 37 + <div class="flex justify-end mt-6"> 38 + <button type="submit" class="btn btn-accent mb-4">{% trans 'Save Changes' %}</button> 39 + </div> 40 + </form> 41 + </div> 42 + </div> 43 + {% endblock %}
+58
fbl_integration/templates/fbl_auth_get_code.html
··· 1 + {% extends 'base.html' %} 2 + {% block content %} 3 + {% load i18n %} 4 + <div class="self-center w-full max-w-xl mx-auto"> 5 + <div class="flex items-center"> 6 + <h1 class="text-3xl font-bold p-2">{% trans 'FurryBlacklight Login' %}</h1> 7 + <div class="flex-grow"></div> 8 + <a href="{% url 'login' %}" class="btn btn-sm btn-neutral">{% trans 'Go Back' %}</a> 9 + </div> 10 + <div class="bg-base-200 rounded p-8"> 11 + <ul class="steps w-full mb-6"> 12 + <li class="step step-accent">{% trans 'Authenticate' %}</li> 13 + <li class="step step-accent">{% trans 'Confirm Code' %}</li> 14 + <li class="step">{% trans 'Account Completion' %}</li> 15 + <li class="step step-success">{% trans 'Done' %}</li> 16 + </ul> 17 + 18 + <div role="alert" class="alert alert-info mb-4"> 19 + {% trans 'We sent you an email that contains a code for your login. Please enter the code in the field below.' %} 20 + </div> 21 + <form method="post" action="/fbl/auth_get_code"> 22 + {% csrf_token %} 23 + 24 + {% if get_code_form.non_field_errors %} 25 + <div role="alert" class="alert alert-error mb-4"> 26 + <svg xmlns="http://www.w3.org/2000/svg" class="hidden sm:block stroke-current shrink-0 h-6 w-6" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10l4 4m0 -4l-4 4" /><path d="M12 3c7.2 0 9 1.8 9 9s-1.8 9 -9 9s-9 -1.8 -9 -9s1.8 -9 9 -9z" /></svg> 27 + <span>{{ get_code_form.non_field_errors }}</span> 28 + </div> 29 + {% endif %} 30 + 31 + <div class="form-control w-full"> 32 + <label class="label"> 33 + <span class="text-base label-text" for="{{ get_code_form.username.id_for_label }}">{% trans 'Badge Number' %}</span> 34 + </label> 35 + <input type="number" name="badge_number" class="w-full input input-bordered" value="{{ get_code_form.badge_number.value }}" readonly /> 36 + </div> 37 + <div class="form-control w-full"> 38 + <label class="label"> 39 + <span class="text-base label-text" for="{{ get_code_form.dob.id_for_label }}">{% trans 'Date of Birth' %}</span> 40 + </label> 41 + <input type="text" name="dob" class="w-full input input-bordered" value="{{ get_code_form.dob.value }}" readonly /> 42 + </div> 43 + 44 + <script src="https://cdn.jsdelivr.net/npm/@alpinejs/mask@3.x.x/dist/cdn.min.js"></script> 45 + <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> 46 + <div class="form-control w-full"> 47 + <label class="label"> 48 + <span class="text-base label-text" for="{{ get_code_form.username.id_for_label }}">{% trans 'Verify Code' %}</span> 49 + </label> 50 + <input type="text" name="validation_code" x-mask="999999" placeholder="000000" class="w-full input input-bordered text-2xl" /> 51 + </div> 52 + <div class="flex justify-end mt-6"> 53 + <button type="submit" class="btn btn-accent mb-4">{% trans 'Confirm Code' %}</button> 54 + </div> 55 + </form> 56 + </div> 57 + </div> 58 + {% endblock %}
+10
fbl_integration/templates/fbl_auth_start.html
··· 1 + <!-- templates/core/login.html --> 2 + {% extends 'base.html' %} 3 + {% block content %} 4 + {% load i18n %} 5 + <div class="self-center w-full max-w-xl mx-auto"> 6 + <div class="bg-base-200 rounded p-8"> 7 + {% include 'partials/attendee_login.html' with fbl_auth_form=form %} 8 + </div> 9 + </div> 10 + {% endblock %}
+21
fbl_integration/templates/partials/attendee_info.html
··· 1 + {% block attendee_info %} 2 + {% load i18n %} 3 + {% if fbl_account %} 4 + <h2 class="font-semibold text-xs mt-4 mb-2">{% trans 'Rawrgister Info' %}</h2> 5 + 6 + <div class="text-sm flex flex-col"> 7 + <div class="my-1 flex items-center"> 8 + {{ fbl_account.username }} <div class="ml-2 badge badge-accent">#{{ fbl_account.badge_number }}</div> 9 + </div> 10 + <div class="my-1 flex items-center"> 11 + <label class="font-semibold text-xs">{% trans 'Status' %}:</label> <div class="ml-2 badge badge-info">{{ fbl_account.status }}</div> 12 + </div> 13 + <div> 14 + <h3 class="font-semibold text-xs my-1">{% trans 'Tags' %}</h3> 15 + {% for tag in fbl_account.tags_list %} 16 + <div class="badge badge-accent badge-outline mr-2">{{ tag }}</div> 17 + {% endfor %} 18 + </div> 19 + </div> 20 + {% endif %} 21 + {% endblock %}
+32
fbl_integration/templates/partials/attendee_login.html
··· 1 + {% block attendee_login %} 2 + {% load i18n %} 3 + <form method="post" action="/fbl/auth_start"> 4 + {% csrf_token %} 5 + 6 + {% if fbl_auth_form.non_field_errors %} 7 + <div role="alert" class="alert alert-error mb-4"> 8 + <svg xmlns="http://www.w3.org/2000/svg" class="hidden sm:block stroke-current shrink-0 h-6 w-6" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10l4 4m0 -4l-4 4" /><path d="M12 3c7.2 0 9 1.8 9 9s-1.8 9 -9 9s-9 -1.8 -9 -9s1.8 -9 9 -9z" /></svg> 9 + <span>{{ fbl_auth_form.non_field_errors }}</span> 10 + </div> 11 + {% endif %} 12 + 13 + <div> 14 + <label class="label"> 15 + <span class="text-base label-text" for="{{ fbl_auth_form.username.id_for_label }}">{% trans 'Badge Number' %}</span> 16 + </label> 17 + <input type="number" name="badge_number" placeholder="420" class="w-full input input-bordered" /> 18 + </div> 19 + <script src="https://cdn.jsdelivr.net/npm/@alpinejs/mask@3.x.x/dist/cdn.min.js"></script> 20 + <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> 21 + <label class="form-control w-full"> 22 + <div class="label"> 23 + <span class="text-base label-text" for="{{ fbl_auth_form.dob.id_for_label }}">{% trans 'Date of Birth' %}</span> 24 + </div> 25 + <input type="text" x-mask="99/99/9999" name="dob" placeholder="DD/MM/YYYY" class="w-full input input-bordered"> 26 + </label> 27 + <div class="flex justify-end mt-6"> 28 + <button type="submit" class="btn btn-accent mb-4">{% trans 'Log In with FBL' %}</button> 29 + </div> 30 + </form> 31 + {% if general_login %}<div class="divider">{% trans "I don\'t have a seat for this years FBL" %}</div>{% endif %} 32 + {% endblock %}
+1
fbl_integration/templates/partials/fbl_logo.html
··· 1 + <svg version="1.1" xml:space="preserve" class="w-full h-full" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2177.33 277.33"><defs><clipPath clipPathUnits="userSpaceOnUse" id="a"><path d="M0 456h1920V0H0z"/></clipPath></defs><g clip-path="url(#a)" transform="matrix(1.33333 0 0 -1.33333 -191.33 442.67)"><path d="M378.41 303.43h68.51v-.23c-6.1-5.61-11.87-8.42-17.3-8.42H407.1c-3.15 0-5.12-1.21-5.92-3.64l-1.36-4.1h35.5v-.23c-6.82-5.91-12.44-8.87-16.84-8.87h-21.85c-.3 0-1.74-4.1-4.32-12.3-.84-2.38-1.45-3.75-1.82-4.09H370.9l-.45.23a936.16 936.16 0 0111.6 34.82v1.37c0 1.55-1.21 3.3-3.64 5.23z" fill="#000" fill-opacity="1" fill-rule="nonzero"/><path d="M442.71 303.43h17.3c5.16 0 7.74-1.6 7.74-4.78v-.91c0-.99-2.2-7.9-6.6-20.71-.76-2.5-1.14-4.1-1.14-4.78.45-1.07 1.14-1.6 2.05-1.6h21.84c2.93 0 5.05 3.27 6.38 9.8 3.34 9.36 5 14.82 5 16.38v.23c0 2.69-1.29 4.66-3.86 5.91v.46h18.66c3.07 0 5.12-1.37 6.14-4.1v-2.05c-5.3-17.56-8.57-27.2-9.78-28.9-3.76-4.55-13.17-6.83-28.23-6.83H460.7c-14.87 0-22.3 2.58-22.3 7.74 0 .5 2.8 9.44 8.42 26.85v1.83c0 2-1.37 3.67-4.1 5zM617.45 287.04h33.23c1.06 3.19 1.6 5 1.6 5.46v.69c0 1.06-.54 1.59-1.6 1.59h-25.95c-3.15 0-5.12-1.21-5.92-3.64zm-21.4 16.39h66.24c4.24 0 7.66-2.05 10.24-6.15.3-1.44.45-2.58.45-3.41 0-2.7-1.14-5.96-3.41-9.79-4.67-4.1-12.48-6.14-23.44-6.14v-.23c5.69-3.45 13.8-10.28 24.35-20.48 4.4-5.2 6.6-7.86 6.6-7.97.6-.76.91-1.21.91-1.37h-.23c-26.7 15.82-46.8 25.84-60.31 30.05h-3.19c-.3 0-1.74-4.1-4.32-12.3-.84-2.38-1.44-3.75-1.82-4.09h-19.58l-.45.23a934.2 934.2 0 0111.6 34.82v1.37c0 1.55-1.2 3.3-3.64 5.23z" fill="#000" fill-opacity="1" fill-rule="nonzero"/><path d="M673.5 303.43h21.16c9.45-6.26 14.6-9.75 15.48-10.47 10.4 6.98 16 10.47 16.84 10.47h30.27v-.46a3333.1 3333.1 0 01-91.27-55.08h-.22v.23c3.75 7.06 10.58 15.93 20.48 26.63.19 0 2.39 1.97 6.6 5.92l7.28 5.69-26.63 16.61z" fill="#000" fill-opacity="1" fill-rule="nonzero"/><path d="M657.31 223.71h53.86c.62 0 1.74 3.22 3.35 9.66v1.12c0 1.73-.87 2.6-2.6 2.6h-47.55c-2.66 0-4.64-3.35-5.94-10.03-.38 0-.75-1.12-1.12-3.35m-4.83-13.37c0-.56-.99-3.78-2.97-9.66.37-1.98 1.36-2.97 2.97-2.97h47.55c2.72 0 4.58 2.97 5.57 8.92.25 0 .74 1.23 1.49 3.71zm-29.34 41.23h104.38c10.46 0 17.27-3.96 20.43-11.88.24-1.68.37-3.04.37-4.09-1.3-7.68-2.54-11.52-3.72-11.52-1.6-2.72-5.32-4.82-11.14-6.3h-.37v-.38c4.4 0 7.12-1.86 8.17-5.57v-1.12c-4.15-15.35-7.62-23.03-10.4-23.03-3.9-2.97-7.86-4.45-11.89-4.45H610.14c12.38 37.08 18.57 56.02 18.57 56.83v3.71c0 2.42-1.86 4.77-5.57 7.06zM753.15 251.57h32.31c4.15 0 6.63-2.6 7.43-7.8 0-2.85-4.45-17.08-13.37-42.72v-.74c0-1.11 1.36-1.98 4.09-2.6h38.63c11.08 0 19.62-4.58 25.63-13.74v-.74H740.89l-.74.37c7.18 20.3 13.5 39.25 18.94 56.83v2.23c0 2.54-1.98 5.38-5.94 8.54z" fill="#000" fill-opacity="1" fill-rule="nonzero"/><path d="M892.81 220h54.6c2.73 8.1 4.1 12.57 4.1 13.37v1.12c0 1.73-.87 2.6-2.6 2.6h-47.55c-2.67 0-4.65-3.35-5.95-10.03-.18 0-1.05-2.36-2.6-7.06m-32.68 31.57H964.5c10.47 0 17.28-3.96 20.43-11.88.25-1.68.38-3.04.38-4.09 0-4.83-5.45-22.29-16.35-52.37H935.9l-.74.37c4.46 12.07 7.06 19.5 7.8 22.28h-54.23c-.5 0-2.85-6.68-7.06-20.05-.68-1.74-1.18-2.6-1.49-2.6h-33.05c12.38 37.08 18.57 56.02 18.57 56.83v3.71c0 2.42-1.86 4.77-5.58 7.06zM988.83 251.57h112.55v-.37c-8.6-9.41-20.37-14.11-35.28-14.11h-33.06c-4.03 0-6.63-3.6-7.8-10.78-5.45-15.1-8.18-23.65-8.18-25.63.75-1.73 1.86-2.6 3.35-2.6h27.11c16.97 0 27.24-2.23 30.83-6.68 2.42-2.1 4.52-4.7 6.32-7.8l-.74-.37h-65.75c-24.27 0-36.4 4.2-36.4 12.62 0 .8 4.58 15.42 13.74 43.84v2.97c0 3.28-2.23 6-6.69 8.17z" fill="#000" fill-opacity="1" fill-rule="nonzero"/><path d="M1098.5 251.57h32.32c4.15 0 6.62-2.6 7.43-7.8 0-3.1-2.97-12.88-8.91-29.34l55.71 37.14h34.18v-.37c-3.78-2.78-21.12-13.8-52-33.06 13.18-7.06 30.39-20.68 51.63-40.86 6.68-7.49 11.14-12.94 13.37-16.34h-.37c-47.86 28.17-82.4 45.13-103.64 50.89-6-19.07-9.35-28.6-10.03-28.6h-31.94l-.75.37a1524.9 1524.9 0 0118.95 56.83v2.23c0 2.54-1.98 5.38-5.95 8.54zM1262.6 251.57h32.31c4.15 0 6.62-2.6 7.43-7.8 0-2.85-4.46-17.08-13.37-42.72v-.74c0-1.11 1.36-1.98 4.08-2.6h38.63c11.09 0 19.63-4.58 25.64-13.74v-.74h-106.98l-.75.37a1524.9 1524.9 0 0118.95 56.83v2.23c0 2.54-1.98 5.38-5.95 8.54z" fill="#000" fill-opacity="1" fill-rule="nonzero"/><path d="M1369.2 251.57h32.32c4.15 0 6.62-2.6 7.43-7.8 0-2.85-4.7-17.83-14.12-44.94-3.34-9.85-5.32-15.05-5.94-15.6h-31.95l-.74.37c7.18 20.3 13.5 39.25 18.94 56.83v2.23c0 2.54-1.98 5.38-5.94 8.54zM1428.36 251.57h95.83v-.37c-8.92-9.9-22.29-14.86-40.12-14.86h-30.46c-3.15 0-5.38-3.47-6.68-10.4-.25-.3-3.35-9.35-9.29-27.11h52.38c2.47 0 4.45 2.47 5.94 7.43 0 2.72-2.1 4.08-6.32 4.08h-41.23v.74c11.7 8.92 20.5 13.38 26.38 13.38h26.74c19.56-1.24 29.35-5.08 29.35-11.52-2.98-13.12-5.45-19.68-7.43-19.68-6-6.7-20.5-10.03-43.46-10.03h-49.03c-17.53 1.11-27.06 4.2-28.6 9.28a8.61 8.61 0 00-.75 3.34c9.9 32.32 15.35 49.65 16.34 52 2.36 2.48 5.82 3.72 10.4 3.72M1552.24 251.57h32.31c.5-.06.74-.3.74-.74l-8.54-26.37h42.72c5.45 17.52 8.54 26.56 9.28 27.11h31.95c.5-.06.74-.3.74-.74-13.12-41.1-20.43-63.64-21.91-67.6h-33.06c-.5 0-.75.24-.75.74a483.9 483.9 0 018.55 26.37h-42.35c-5.7-18.08-8.79-27.11-9.28-27.11h-33.43c14.6 45.56 22.28 68.34 23.03 68.34M1663.95 251.57h112.55v-.37c-8.8-9.41-20.3-14.11-34.54-14.11l-18.58-53.86h-32.31l-.75.37 18.2 53.49h-.74c-18.94 0-33.55 4.7-43.83 14.11zM541.15 291.14c.8 2.43 2.76 3.64 5.91 3.64h25.95c1.06 0 1.6-.53 1.6-1.6v-.68c0-.45-.54-2.27-1.6-5.46h-33.23zm65.27-110.6l1.18 3.53c8.13 24.37 12.82 38.6 15.44 46.69-8.27 14.48-25.08 19-54.58 46.95v.23c10.96 0 18.77 2.05 23.44 6.14 2.28 3.83 3.41 7.1 3.41 9.79 0 .83-.15 1.97-.45 3.41-2.58 4.1-6 6.15-10.24 6.15h-66.23v-.23c2.42-1.93 3.64-3.68 3.64-5.23v-1.37a935.52 935.52 0 00-11.61-34.82l.46-.23h19.57c.38.34.98 1.7 1.82 4.1 2.58 8.2 4.02 12.29 4.32 12.29h3.19c13.5-4.21 37.18-16.15 47.9-23.29 15.29-10.2 35.7-38.38 7.13-49.09 4.62-1.05 8.3-.13 11.25 1.87-28.2-42.92-56.77-9-56.77-2.76-16.95-47.3 35.7-49.98 43.74-43.73-2.23-5.86-11.6-8.03-14.28-8.03 19.89-2.85 40.33 12.97 47.85 27.64zM202.8 242.6c-.83 13.59 9.06 26.35 22.65 37.06-.83 4.12.82 9.88.82 9.88s-24.7-11.53-30.47-31.3c2.06-4.94 1.23-11.52 1.23-11.52s-1.23 13.17-9.06 21.4c0 0-1.64-17.7-7.4-29.23-5.77-11.53-9.48-31.3 8.64-48.18a17.74 17.74 0 003.7 2.47s-22.23 14-8.23 38.71c0 0 .41 3.7 8.65-4.12.82 3.3 3.04 11.4 9.47 14.83M278.16 184.53s2.06-2.88.82-7c3.3 1.65 6.18 6.6 6.18 6.6s-4.53 1.23-7 .4M313.57 197.3c2.47 4.12 1.65 11.12-3.3 9.47 6.09 5.06 11.54-1.23 3.3-9.47m-6.59 19.35c-11.53-16.88 5.36-24.7 7-23.47 22.24 18.12 8.25 25.88-7 23.47M263.33 245.48c6.6 2.88 5.77 7.41 7.83 21 7.82 10.7 26.35 4.53 26.35 4.53s-11.53-.82-14-6.59c1.65 2.89 12.36 2.47 16.89 2.47 4.53 0 13.58 6.18 9.06 15.65 8.23-.41 17.7-27.59-17.3-42.41-3.7-1.65-6.59-3.71-11.12-1.24-4.53 2.47-13.18 7-17.7 6.59M253.45 220.36s2.06 4.94 5.35 4.12c3.3-.82 2.89-4.94 2.89-4.94s-4.95 1.23-8.24.82" fill="#000" fill-opacity="1" fill-rule="nonzero"/><path d="M270.33 217.89c-1.23-2.88-7.4.41-12.35 1.23-4.94.83-9.47-1.64-9.47 1.65 0 3.3 10.35 9.78 17.3 8.24 7.4-1.65 5.76-8.24 4.52-11.12m-34.18 35.41c-1.64-12.76-7.82-6.58-7-2.47 4.19 20.92 34.18 41.6 34.18 41.6-11.53-10.3-25.53-26.36-27.18-39.13m69.19-35.82s-19.36 5.35-24.71 1.64c-5.35-3.7-3.7-.4-.82 1.24-.36.5 3.16 8.03 4.11 12.35 1.31 5.94-17.7 10.71-22.65 11.12-4.94.41-15.64-5.35-15.64-5.35s0 2.06 11.11 6.59c17.98 7.32 6.18 19.76 20.18 51.47 10.26 23.24-10.47 14.92-50.65-19.76-39.12-33.77-14-51.07-7.41-49.42-8.24-2.47-15.24 5.35-15.78 12.44-4.81-1.73-8.52-12.85-8.52-12.85 1.65-2.88 6.18-6.59 8.65-7.83-8.24 1.65-15.92 13.52-17.7 10.3-10.3-18.53 4.52-32.94 13.17-37.06-10.3-1.65-10.7-8.65-10.7-8.65 13.58 6.18 17.7-4.94 26.76-5.76-10.7 11.94 4.12 26.35 4.12 26.35s-10.3-16.06-.41-25.12c14.62-13.4 18.12 4.53 55.18-11.53 16.92-7.33 23.88 8.65 25.12 9.88 1.23 1.24-7 5.77-12.36 5.77a19.86 19.86 0 00-8.23-7.83s.41 5.36-2.47 8.24c-2.47 0-13.18 0-19.36 5.77-6.17 5.76-14.82 9.88-21 10.3 11.94 2.87 21-3.72 21-3.72s-4.53.83-4.94 0c6.18-2.88 3.3-10.29 20.18-9.88 18.95.46 28.41-6.59 28.41-6.59s8.24 4.12 10.3 11.53c-17.7 13.18-4.94 26.36-4.94 26.36" fill="#000" fill-opacity="1" fill-rule="nonzero"/><path d="M253.68 136.35c-54.03 0-97.83 43.8-97.83 97.83 0 53 42.17 96.16 94.8 97.77-1.05.03-2.1.05-3.15.05-57.44 0-104-46.56-104-104s46.56-104 104-104 104 46.56 104 104c0 1.05-.02 2.1-.05 3.14-1.6-52.62-44.76-94.79-97.77-94.79" fill="#000" fill-opacity="1" fill-rule="nonzero"/></g></svg>
+3
fbl_integration/tests.py
··· 1 + from django.test import TestCase 2 + 3 + # Create your tests here.
+9
fbl_integration/urls.py
··· 1 + from django.urls import path 2 + 3 + from .views import fbl_authentication_start, fbl_authentication_get_code, complete_registration 4 + 5 + urlpatterns = [ 6 + path("auth_start", fbl_authentication_start, name="fbl_auth_start"), 7 + path("auth_get_code", fbl_authentication_get_code, name="fbl_auth_get_code"), 8 + path("complete_registration", complete_registration, name="fbl_complete_registration"), 9 + ]
+55
fbl_integration/utils.py
··· 1 + import requests 2 + from django.conf import settings 3 + from .models import FblAccount 4 + 5 + def fbl_auth_request_code(badge_number: int, dob: str) -> bool: 6 + res = requests.post(f'{settings.FBL_AUTH_SERVER}/auth/get-code', data={ 7 + "badge": badge_number, 8 + "password": dob 9 + }) 10 + if res.status_code != 201: 11 + print(res.status_code) 12 + return False 13 + return True 14 + 15 + def fbl_auth_validate_code(badge_number: str, dob: str, validation_code: str) -> str | None: 16 + res = requests.post(f'{settings.FBL_AUTH_SERVER}/auth/signin', json={ 17 + "badge": badge_number, 18 + "password": dob, 19 + "code": validation_code 20 + }) 21 + if res.status_code != 201: 22 + print(res.status_code) 23 + return None 24 + 25 + return res.json()["accessToken"] 26 + 27 + def fbl_auth_get_account(jwt_token: str) -> dict[str, str] | None: 28 + res = requests.get(f'{settings.FBL_AUTH_SERVER}/attendees/me', headers={ 29 + "Authorization": f"Bearer {jwt_token}" 30 + }) 31 + if res.status_code != 200: 32 + print(res.status_code) 33 + return None 34 + 35 + return res.json() 36 + 37 + def get_or_create_account(account_info: dict[str, str]) -> FblAccount: 38 + """Get or create a FblAccount object based on the account info""" 39 + if FblAccount.objects.filter(account_id=account_info["account_id"]).exists(): 40 + # Get existing user and update status 41 + user = FblAccount.objects.get(account_id=account_info["account_id"]) 42 + user.status = account_info["status"] 43 + user.save() 44 + return user 45 + 46 + # Create new user with (generic) username 47 + user = FblAccount.create_user(account_info["username"]) 48 + return FblAccount.objects.create( 49 + user=user, 50 + badge_number=account_info["badge"], 51 + account_id=account_info["account_id"], 52 + status=account_info["status"], 53 + username=account_info["username"], 54 + tags_secured=','.join(account_info["tags_secured"]) 55 + )
+62
fbl_integration/views.py
··· 1 + from django.shortcuts import render, redirect 2 + from django.contrib.auth import login 3 + from datetime import datetime 4 + from .forms import FblAuthForm, FblAuthCodeForm, RegistrationCompletionForm 5 + from .utils import fbl_auth_request_code, fbl_auth_validate_code, fbl_auth_get_account, get_or_create_account 6 + 7 + def fbl_authentication_start(request): 8 + print('fbl auth started') 9 + auth_form = FblAuthForm(request.POST or None) 10 + if request.method == "POST": 11 + if auth_form.is_valid(): 12 + 13 + valid_info = fbl_auth_request_code( 14 + badge_number=auth_form.cleaned_data["badge_number"], 15 + dob=auth_form.cleaned_data["dob"] 16 + ) 17 + if valid_info: 18 + code_form = FblAuthCodeForm(initial={ 19 + **auth_form.cleaned_data, 20 + "dob": datetime.strptime(auth_form.cleaned_data["dob"], "%Y-%m-%d").strftime("%d/%m/%Y"), 21 + }) 22 + 23 + return render(request, "fbl_auth_get_code.html", {"get_code_form": code_form}) 24 + 25 + auth_form.add_error(None, "Invalid badge number or date of birth") 26 + 27 + return render(request, "fbl_auth_start.html", {"form": auth_form}) 28 + 29 + 30 + def fbl_authentication_get_code(request): 31 + code_form = FblAuthCodeForm(request.POST or None) 32 + if request.method == "POST": 33 + if code_form.is_valid(): 34 + access_token = fbl_auth_validate_code( 35 + badge_number=code_form.cleaned_data["badge_number"], 36 + dob=code_form.cleaned_data["dob"], 37 + validation_code=code_form.cleaned_data["validation_code"] 38 + ) 39 + if access_token: 40 + account_info = fbl_auth_get_account(access_token) 41 + fbl_account = get_or_create_account(account_info) 42 + 43 + login(request=request, user=fbl_account.user) 44 + 45 + if not fbl_account.user.email: 46 + return redirect("fbl_complete_registration") 47 + else: 48 + return redirect("all_tickets") 49 + 50 + code_form.add_error(None, "Invalid validation code") 51 + 52 + 53 + return render(request, "fbl_auth_get_code.html", {"get_code_form": code_form}) 54 + 55 + def complete_registration(request): 56 + form = RegistrationCompletionForm(request.POST or None) 57 + if request.method == "POST": 58 + if form.is_valid(): 59 + request.user.email = form.cleaned_data["email"] 60 + request.user.save() 61 + return redirect("all_tickets") 62 + return render(request, "complete_registration.html", {"form": form})
+3 -2
paw/__init__.py
··· 1 1 from django import get_version 2 2 3 - VERSION = (0, 5, 9, "final", 0) 3 + PAW_VERSION = (0, 5, 9, "final", 0) 4 + FBL_ITERATION = 4 4 5 5 - __version__ = get_version(VERSION) 6 + __version__ = f"{get_version(PAW_VERSION)}-fbl{FBL_ITERATION}"
+9 -1
paw/settings.py
··· 45 45 "django.contrib.messages", 46 46 "django.contrib.staticfiles", 47 47 "status", 48 + "fbl_integration", 48 49 ] 49 50 50 51 AUTH_USER_MODEL = "core.PawUser" ··· 65 66 TEMPLATES = [ 66 67 { 67 68 "BACKEND": "django.template.backends.django.DjangoTemplates", 68 - "DIRS": [BASE_DIR / 'paw' / 'templates'], 69 + "DIRS": [ 70 + BASE_DIR / 'paw' / 'templates', 71 + BASE_DIR / 'fbl_integration' / 'templates', 72 + ], 69 73 "APP_DIRS": True, 70 74 "OPTIONS": { 71 75 "context_processors": [ ··· 186 190 GOOGLE_OAUTH_CLIENT_SECRET = environ['GOOGLE_OAUTH_CLIENT_SECRET'] 187 191 GOOGLE_OAUTH_REDIRECT_URI = environ['GOOGLE_OAUTH_REDIRECT_URI'] 188 192 GOOGLE_OAUTH_SCOPES = environ.get('GOOGLE_OAUTH_SCOPES', '').split(",") 193 + 194 + # FBL Integration 195 + FBL_AUTH_ENABLED = environ.get('FBL_AUTH_ENABLED').lower() == 'true' 196 + FBL_AUTH_SERVER = environ['FBL_AUTH_SERVER']
+69 -473
paw/static/css/paw.css
··· 1310 1310 } 1311 1311 } 1312 1312 1313 - .btn-outline.btn-primary:hover { 1314 - --tw-text-opacity: 1; 1315 - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); 1316 - } 1317 - 1318 - @supports (color: color-mix(in oklab, black, black)) { 1319 - .btn-outline.btn-primary:hover { 1320 - background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); 1321 - border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); 1322 - } 1323 - } 1324 - 1325 1313 .btn-outline.btn-accent:hover { 1326 1314 --tw-text-opacity: 1; 1327 1315 color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); ··· 1707 1695 height: auto; 1708 1696 } 1709 1697 1710 - .stats { 1711 - display: inline-grid; 1712 - border-radius: var(--rounded-box, 1rem); 1713 - --tw-bg-opacity: 1; 1714 - background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); 1715 - --tw-text-opacity: 1; 1716 - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); 1717 - } 1718 - 1719 - :where(.stats) { 1720 - grid-auto-flow: column; 1721 - overflow-x: auto; 1722 - } 1723 - 1724 - .stat { 1725 - display: inline-grid; 1726 - width: 100%; 1727 - grid-template-columns: repeat(1, 1fr); 1728 - -moz-column-gap: 1rem; 1729 - column-gap: 1rem; 1730 - border-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-border-opacity))); 1731 - --tw-border-opacity: 0.1; 1732 - padding-left: 1.5rem; 1733 - padding-right: 1.5rem; 1734 - padding-top: 1rem; 1735 - padding-bottom: 1rem; 1736 - } 1737 - 1738 - .stat-figure { 1739 - grid-column-start: 2; 1740 - grid-row: span 3 / span 3; 1741 - grid-row-start: 1; 1742 - place-self: center; 1743 - justify-self: end; 1744 - } 1745 - 1746 - .stat-title { 1747 - grid-column-start: 1; 1748 - white-space: nowrap; 1749 - color: var(--fallback-bc,oklch(var(--bc)/0.6)); 1750 - } 1751 - 1752 - .stat-value { 1753 - grid-column-start: 1; 1754 - white-space: nowrap; 1755 - font-size: 2.25rem; 1756 - line-height: 2.5rem; 1757 - font-weight: 800; 1758 - } 1759 - 1760 1698 .steps { 1761 1699 display: inline-grid; 1762 1700 grid-auto-flow: column; ··· 1893 1831 var(--togglehandleborder); 1894 1832 } 1895 1833 1834 + .alert-info { 1835 + border-color: var(--fallback-in,oklch(var(--in)/0.2)); 1836 + --tw-text-opacity: 1; 1837 + color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); 1838 + --alert-bg: var(--fallback-in,oklch(var(--in)/1)); 1839 + --alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1)); 1840 + } 1841 + 1896 1842 .alert-success { 1897 1843 border-color: var(--fallback-su,oklch(var(--su)/0.2)); 1898 1844 --tw-text-opacity: 1; ··· 1943 1889 color: var(--fallback-ac,oklch(var(--ac)/var(--tw-text-opacity))); 1944 1890 } 1945 1891 1892 + .badge-info { 1893 + border-color: transparent; 1894 + --tw-bg-opacity: 1; 1895 + background-color: var(--fallback-in,oklch(var(--in)/var(--tw-bg-opacity))); 1896 + --tw-text-opacity: 1; 1897 + color: var(--fallback-inc,oklch(var(--inc)/var(--tw-text-opacity))); 1898 + } 1899 + 1946 1900 .badge-success { 1947 1901 border-color: transparent; 1948 1902 --tw-bg-opacity: 1; ··· 1967 1921 color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity))); 1968 1922 } 1969 1923 1970 - .badge-ghost { 1971 - --tw-border-opacity: 1; 1972 - border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); 1973 - --tw-bg-opacity: 1; 1974 - background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); 1975 - --tw-text-opacity: 1; 1976 - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); 1924 + .badge-outline { 1925 + border-color: currentColor; 1926 + --tw-border-opacity: 0.5; 1927 + background-color: transparent; 1928 + color: currentColor; 1977 1929 } 1978 1930 1979 1931 .badge-outline.badge-neutral { ··· 1981 1933 color: var(--fallback-n,oklch(var(--n)/var(--tw-text-opacity))); 1982 1934 } 1983 1935 1936 + .badge-outline.badge-primary { 1937 + --tw-text-opacity: 1; 1938 + color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); 1939 + } 1940 + 1941 + .badge-outline.badge-secondary { 1942 + --tw-text-opacity: 1; 1943 + color: var(--fallback-s,oklch(var(--s)/var(--tw-text-opacity))); 1944 + } 1945 + 1984 1946 .badge-outline.badge-accent { 1985 1947 --tw-text-opacity: 1; 1986 1948 color: var(--fallback-a,oklch(var(--a)/var(--tw-text-opacity))); 1987 1949 } 1988 1950 1951 + .badge-outline.badge-info { 1952 + --tw-text-opacity: 1; 1953 + color: var(--fallback-in,oklch(var(--in)/var(--tw-text-opacity))); 1954 + } 1955 + 1989 1956 .badge-outline.badge-success { 1990 1957 --tw-text-opacity: 1; 1991 1958 color: var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity))); ··· 2018 1985 border-color: var(--btn-color, var(--fallback-b2)); 2019 1986 } 2020 1987 2021 - .btn-primary { 2022 - --btn-color: var(--fallback-p); 2023 - } 2024 - 2025 1988 .btn-accent { 2026 1989 --btn-color: var(--fallback-a); 2027 1990 } ··· 2044 2007 } 2045 2008 2046 2009 @supports (color: color-mix(in oklab, black, black)) { 2047 - .btn-outline.btn-primary.btn-active { 2048 - background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); 2049 - border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); 2050 - } 2051 - 2052 2010 .btn-outline.btn-accent.btn-active { 2053 2011 background-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); 2054 2012 border-color: color-mix(in oklab, var(--fallback-a,oklch(var(--a)/1)) 90%, black); ··· 2076 2034 outline-offset: 2px; 2077 2035 } 2078 2036 2079 - .btn-primary { 2080 - --tw-text-opacity: 1; 2081 - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); 2082 - outline-color: var(--fallback-p,oklch(var(--p)/1)); 2083 - } 2084 - 2085 2037 @supports (color: oklch(0 0 0)) { 2086 - .btn-primary { 2087 - --btn-color: var(--p); 2088 - } 2089 - 2090 2038 .btn-accent { 2091 2039 --btn-color: var(--a); 2092 2040 } ··· 2164 2112 .btn-ghost.btn-active { 2165 2113 border-color: transparent; 2166 2114 background-color: var(--fallback-bc,oklch(var(--bc)/0.2)); 2167 - } 2168 - 2169 - .btn-outline.btn-primary { 2170 - --tw-text-opacity: 1; 2171 - color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); 2172 - } 2173 - 2174 - .btn-outline.btn-primary.btn-active { 2175 - --tw-text-opacity: 1; 2176 - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); 2177 2115 } 2178 2116 2179 2117 .btn-outline.btn-accent { ··· 2750 2688 } 2751 2689 } 2752 2690 2753 - :where(.stats) > :not([hidden]) ~ :not([hidden]) { 2754 - --tw-divide-x-reverse: 0; 2755 - border-right-width: calc(1px * var(--tw-divide-x-reverse)); 2756 - border-left-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); 2757 - --tw-divide-y-reverse: 0; 2758 - border-top-width: calc(0px * calc(1 - var(--tw-divide-y-reverse))); 2759 - border-bottom-width: calc(0px * var(--tw-divide-y-reverse)); 2760 - } 2761 - 2762 - :is([dir="rtl"] .stats > :not([hidden]) ~ :not([hidden])) { 2763 - --tw-divide-x-reverse: 1; 2764 - } 2765 - 2766 2691 .steps .step:before { 2767 2692 top: 0px; 2768 2693 grid-column-start: 1; ··· 3397 3322 margin-bottom: 1rem; 3398 3323 } 3399 3324 3325 + .my-1 { 3326 + margin-top: 0.25rem; 3327 + margin-bottom: 0.25rem; 3328 + } 3329 + 3330 + .my-2 { 3331 + margin-top: 0.5rem; 3332 + margin-bottom: 0.5rem; 3333 + } 3334 + 3400 3335 .mb-1 { 3401 3336 margin-bottom: 0.25rem; 3402 3337 } ··· 3419 3354 3420 3355 .ml-2 { 3421 3356 margin-left: 0.5rem; 3422 - } 3423 - 3424 - .ml-20 { 3425 - margin-left: 5rem; 3426 3357 } 3427 3358 3428 3359 .ml-4 { ··· 3453 3384 margin-top: 1rem; 3454 3385 } 3455 3386 3456 - .mt-8 { 3457 - margin-top: 2rem; 3387 + .mt-6 { 3388 + margin-top: 1.5rem; 3458 3389 } 3459 3390 3460 - .mt-6 { 3461 - margin-top: 1.5rem; 3391 + .mt-8 { 3392 + margin-top: 2rem; 3462 3393 } 3463 3394 3464 3395 .block { ··· 3481 3412 display: none; 3482 3413 } 3483 3414 3484 - .h-10 { 3485 - height: 2.5rem; 3486 - } 3487 - 3488 3415 .h-12 { 3489 3416 height: 3rem; 3490 3417 } ··· 3505 3432 height: 1.5rem; 3506 3433 } 3507 3434 3508 - .h-7 { 3509 - height: 1.75rem; 3510 - } 3511 - 3512 3435 .h-full { 3513 3436 height: 100%; 3514 3437 } 3515 3438 3516 - .min-h-screen { 3517 - min-height: 100vh; 3439 + .min-h-full { 3440 + min-height: 100%; 3518 3441 } 3519 3442 3520 - .min-h-full { 3521 - min-height: 100%; 3443 + .min-h-screen { 3444 + min-height: 100vh; 3522 3445 } 3523 3446 3524 3447 .w-10 { ··· 3529 3452 width: 3rem; 3530 3453 } 3531 3454 3532 - .w-20 { 3533 - width: 5rem; 3534 - } 3535 - 3536 3455 .w-4 { 3537 3456 width: 1rem; 3538 3457 } ··· 3549 3468 width: 1.5rem; 3550 3469 } 3551 3470 3552 - .w-7 { 3553 - width: 1.75rem; 3471 + .w-72 { 3472 + width: 18rem; 3554 3473 } 3555 3474 3556 3475 .w-full { 3557 3476 width: 100%; 3558 3477 } 3559 3478 3560 - .w-80 { 3561 - width: 20rem; 3562 - } 3563 - 3564 - .w-72 { 3565 - width: 18rem; 3566 - } 3567 - 3568 3479 .max-w-3xl { 3569 3480 max-width: 48rem; 3570 3481 } ··· 3601 3512 flex-grow: 1; 3602 3513 } 3603 3514 3515 + .transform { 3516 + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 3517 + } 3518 + 3604 3519 .cursor-pointer { 3605 3520 cursor: pointer; 3606 3521 } ··· 3643 3558 3644 3559 .self-center { 3645 3560 align-self: center; 3646 - } 3647 - 3648 - .overflow-y-auto { 3649 - overflow-y: auto; 3650 3561 } 3651 3562 3652 3563 .whitespace-pre-line { ··· 3717 3628 background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); 3718 3629 } 3719 3630 3720 - .bg-base-content { 3721 - --tw-bg-opacity: 1; 3722 - background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); 3723 - } 3724 - 3725 3631 .stroke-current { 3726 3632 stroke: currentColor; 3727 3633 } ··· 3751 3657 padding-right: 0.5rem; 3752 3658 } 3753 3659 3660 + .px-3 { 3661 + padding-left: 0.75rem; 3662 + padding-right: 0.75rem; 3663 + } 3664 + 3754 3665 .py-1 { 3755 3666 padding-top: 0.25rem; 3756 3667 padding-bottom: 0.25rem; ··· 3759 3670 .py-2 { 3760 3671 padding-top: 0.5rem; 3761 3672 padding-bottom: 0.5rem; 3762 - } 3763 - 3764 - .px-3 { 3765 - padding-left: 0.75rem; 3766 - padding-right: 0.75rem; 3767 3673 } 3768 3674 3769 3675 .text-center { ··· 3835 3741 color: var(--fallback-bc,oklch(var(--bc)/0.85)); 3836 3742 } 3837 3743 3838 - .text-error { 3839 - --tw-text-opacity: 1; 3840 - color: var(--fallback-er,oklch(var(--er)/var(--tw-text-opacity))); 3841 - } 3842 - 3843 3744 .text-neutral-content { 3844 3745 --tw-text-opacity: 1; 3845 3746 color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); 3846 3747 } 3847 3748 3848 - .text-success { 3849 - --tw-text-opacity: 1; 3850 - color: var(--fallback-su,oklch(var(--su)/var(--tw-text-opacity))); 3851 - } 3852 - 3853 - .text-warning { 3854 - --tw-text-opacity: 1; 3855 - color: var(--fallback-wa,oklch(var(--wa)/var(--tw-text-opacity))); 3856 - } 3857 - 3858 3749 .text-white { 3859 3750 --tw-text-opacity: 1; 3860 3751 color: rgb(255 255 255 / var(--tw-text-opacity)); ··· 3868 3759 opacity: 0.5; 3869 3760 } 3870 3761 3871 - .opacity-70 { 3872 - opacity: 0.7; 3873 - } 3874 - 3875 3762 .opacity-60 { 3876 3763 opacity: 0.6; 3877 3764 } 3878 3765 3879 - @media (min-width: 1024px) { 3880 - .lg\:btn { 3881 - display: inline-flex; 3882 - height: 3rem; 3883 - min-height: 3rem; 3884 - flex-shrink: 0; 3885 - cursor: pointer; 3886 - -webkit-user-select: none; 3887 - -moz-user-select: none; 3888 - user-select: none; 3889 - flex-wrap: wrap; 3890 - align-items: center; 3891 - justify-content: center; 3892 - border-radius: var(--rounded-btn, 0.5rem); 3893 - border-color: transparent; 3894 - border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity)); 3895 - padding-left: 1rem; 3896 - padding-right: 1rem; 3897 - text-align: center; 3898 - font-size: 0.875rem; 3899 - line-height: 1em; 3900 - gap: 0.5rem; 3901 - font-weight: 600; 3902 - text-decoration-line: none; 3903 - transition-duration: 200ms; 3904 - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); 3905 - border-width: var(--border-btn, 1px); 3906 - animation: button-pop var(--animation-btn, 0.25s) ease-out; 3907 - transition-property: color, background-color, border-color, opacity, box-shadow, transform; 3908 - --tw-text-opacity: 1; 3909 - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); 3910 - --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 3911 - --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); 3912 - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 3913 - outline-color: var(--fallback-bc,oklch(var(--bc)/1)); 3914 - background-color: oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity)); 3915 - --tw-bg-opacity: 1; 3916 - --tw-border-opacity: 1; 3917 - } 3918 - 3919 - .lg\:btn[disabled],.lg\:btn:disabled { 3920 - pointer-events: none; 3921 - } 3922 - 3923 - :where(.lg\:btn:is(input[type="checkbox"])), 3924 - :where(.lg\:btn:is(input[type="radio"])) { 3925 - width: auto; 3926 - -webkit-appearance: none; 3927 - -moz-appearance: none; 3928 - appearance: none; 3929 - } 3930 - 3931 - .lg\:btn:is(input[type="checkbox"]):after,.lg\:btn:is(input[type="radio"]):after { 3932 - --tw-content: attr(aria-label); 3933 - content: var(--tw-content); 3934 - } 3935 - 3936 - @media (hover: hover) { 3937 - .lg\:btn:hover { 3938 - --tw-border-opacity: 1; 3939 - border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); 3940 - --tw-bg-opacity: 1; 3941 - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); 3942 - } 3943 - 3944 - @supports (color: color-mix(in oklab, black, black)) { 3945 - .lg\:btn:hover { 3946 - background-color: color-mix( 3947 - in oklab, 3948 - oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, 3949 - black 3950 - ); 3951 - border-color: color-mix( 3952 - in oklab, 3953 - oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, 3954 - black 3955 - ); 3956 - } 3957 - } 3958 - 3959 - @supports not (color: oklch(0 0 0)) { 3960 - .lg\:btn:hover { 3961 - background-color: var(--btn-color, var(--fallback-b2)); 3962 - border-color: var(--btn-color, var(--fallback-b2)); 3963 - } 3964 - } 3965 - 3966 - .lg\:btn:hover { 3967 - --tw-border-opacity: 1; 3968 - border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); 3969 - --tw-bg-opacity: 1; 3970 - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); 3971 - } 3972 - 3973 - @supports (color: color-mix(in oklab, black, black)) { 3974 - .lg\:btn:hover { 3975 - background-color: color-mix( 3976 - in oklab, 3977 - oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, 3978 - black 3979 - ); 3980 - border-color: color-mix( 3981 - in oklab, 3982 - oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, 3983 - black 3984 - ); 3985 - } 3986 - } 3987 - 3988 - @supports not (color: oklch(0 0 0)) { 3989 - .lg\:btn:hover { 3990 - background-color: var(--btn-color, var(--fallback-b2)); 3991 - border-color: var(--btn-color, var(--fallback-b2)); 3992 - } 3993 - } 3994 - 3995 - .lg\:btn:hover { 3996 - --tw-border-opacity: 1; 3997 - border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); 3998 - --tw-bg-opacity: 1; 3999 - background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); 4000 - } 4001 - 4002 - @supports (color: color-mix(in oklab, black, black)) { 4003 - .lg\:btn:hover { 4004 - background-color: color-mix( 4005 - in oklab, 4006 - oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, 4007 - black 4008 - ); 4009 - border-color: color-mix( 4010 - in oklab, 4011 - oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, 4012 - black 4013 - ); 4014 - } 4015 - } 4016 - 4017 - @supports not (color: oklch(0 0 0)) { 4018 - .lg\:btn:hover { 4019 - background-color: var(--btn-color, var(--fallback-b2)); 4020 - border-color: var(--btn-color, var(--fallback-b2)); 4021 - } 4022 - } 4023 - 4024 - .lg\:btn.glass:hover { 4025 - --glass-opacity: 25%; 4026 - --glass-border-opacity: 15%; 4027 - } 3766 + .opacity-70 { 3767 + opacity: 0.7; 3768 + } 4028 3769 4029 - .lg\:btn[disabled]:hover,.lg\:btn:disabled:hover { 4030 - --tw-border-opacity: 0; 4031 - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); 4032 - --tw-bg-opacity: 0.2; 4033 - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); 4034 - --tw-text-opacity: 0.2; 4035 - } 4036 - 4037 - @supports (color: color-mix(in oklab, black, black)) { 4038 - .lg\:btn:is(input[type="checkbox"]:checked):hover,.lg\:btn:is(input[type="radio"]:checked):hover { 4039 - background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); 4040 - border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); 4041 - } 4042 - } 4043 - } 4044 - 4045 - .lg\:btn:active:hover,.lg\:btn:active:focus { 4046 - animation: button-pop 0s ease-out; 4047 - transform: scale(var(--btn-focus-scale, 0.97)); 4048 - } 4049 - 4050 - @supports not (color: oklch(0 0 0)) { 4051 - .lg\:btn { 4052 - background-color: var(--btn-color, var(--fallback-b2)); 4053 - border-color: var(--btn-color, var(--fallback-b2)); 4054 - } 4055 - } 4056 - 4057 - .lg\:btn:focus-visible { 4058 - outline-style: solid; 4059 - outline-width: 2px; 4060 - outline-offset: 2px; 4061 - } 4062 - 4063 - .lg\:btn.glass { 4064 - --tw-shadow: 0 0 #0000; 4065 - --tw-shadow-colored: 0 0 #0000; 4066 - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 4067 - outline-color: currentColor; 4068 - } 4069 - 4070 - .lg\:btn.glass.btn-active { 4071 - --glass-opacity: 25%; 4072 - --glass-border-opacity: 15%; 4073 - } 4074 - 4075 - .lg\:btn.btn-disabled,.lg\:btn[disabled],.lg\:btn:disabled { 4076 - --tw-border-opacity: 0; 4077 - background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); 4078 - --tw-bg-opacity: 0.2; 4079 - color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); 4080 - --tw-text-opacity: 0.2; 4081 - } 4082 - 4083 - .lg\:btn:is(input[type="checkbox"]:checked),.lg\:btn:is(input[type="radio"]:checked) { 4084 - --tw-border-opacity: 1; 4085 - border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); 4086 - --tw-bg-opacity: 1; 4087 - background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); 4088 - --tw-text-opacity: 1; 4089 - color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); 4090 - } 4091 - 4092 - .lg\:btn:is(input[type="checkbox"]:checked):focus-visible,.lg\:btn:is(input[type="radio"]:checked):focus-visible { 4093 - outline-color: var(--fallback-p,oklch(var(--p)/1)); 4094 - } 4095 - 4096 - .lg\:btn-sm { 4097 - height: 2rem; 4098 - min-height: 2rem; 4099 - padding-left: 0.75rem; 4100 - padding-right: 0.75rem; 4101 - font-size: 0.875rem; 4102 - } 4103 - 4104 - .lg\:btn-lg { 4105 - height: 4rem; 4106 - min-height: 4rem; 4107 - padding-left: 1.5rem; 4108 - padding-right: 1.5rem; 4109 - font-size: 1.125rem; 4110 - } 4111 - 4112 - .btn-square:where(.lg\:btn-sm) { 4113 - height: 2rem; 4114 - width: 2rem; 4115 - padding: 0px; 4116 - } 4117 - 4118 - .btn-square:where(.lg\:btn-lg) { 4119 - height: 4rem; 4120 - width: 4rem; 4121 - padding: 0px; 4122 - } 4123 - 4124 - .btn-circle:where(.lg\:btn-sm) { 4125 - height: 2rem; 4126 - width: 2rem; 4127 - border-radius: 9999px; 4128 - padding: 0px; 4129 - } 4130 - 4131 - .btn-circle:where(.lg\:btn-lg) { 4132 - height: 4rem; 4133 - width: 4rem; 4134 - border-radius: 9999px; 4135 - padding: 0px; 4136 - } 4137 - 3770 + @media (min-width: 1024px) { 4138 3771 .lg\:drawer-open > .drawer-toggle { 4139 3772 display: none; 4140 3773 } ··· 4189 3822 @media (min-width: 1024px) { 4190 3823 .lg\:order-none { 4191 3824 order: 0; 4192 - } 4193 - 4194 - .lg\:ml-72 { 4195 - margin-left: 18rem; 4196 3825 } 4197 3826 4198 3827 .lg\:block { 4199 3828 display: block; 4200 3829 } 4201 3830 4202 - .lg\:flex { 4203 - display: flex; 4204 - } 4205 - 4206 3831 .lg\:hidden { 4207 3832 display: none; 4208 3833 } 4209 3834 4210 - .lg\:w-72 { 4211 - width: 18rem; 4212 - } 4213 - 4214 - .lg\:w-full { 4215 - width: 100%; 4216 - } 4217 - 4218 - .lg\:max-w-md { 4219 - max-width: 28rem; 4220 - } 4221 - 4222 3835 .lg\:max-w-sm { 4223 3836 max-width: 24rem; 4224 3837 } 4225 3838 4226 3839 .lg\:flex-row { 4227 3840 flex-direction: row; 4228 - } 4229 - 4230 - .lg\:justify-start { 4231 - justify-content: flex-start; 4232 - } 4233 - 4234 - .lg\:p-2 { 4235 - padding: 0.5rem; 4236 - } 4237 - 4238 - .lg\:p-4 { 4239 - padding: 1rem; 4240 - } 4241 - 4242 - .lg\:px-3 { 4243 - padding-left: 0.75rem; 4244 - padding-right: 0.75rem; 4245 3841 } 4246 3842 }
+7 -1
paw/templates/core/login.html
··· 3 3 {% block content %} 4 4 {% load i18n %} 5 5 <div class="self-center w-full max-w-xl mx-auto"> 6 + <div class="p-4"> 7 + {% include 'partials/fbl_logo.html' %} 8 + </div> 6 9 <div class="flex items-center"> 7 10 <h1 class="text-3xl font-bold p-2">{% trans 'Log In' %}</h1> 8 11 <div class="flex-grow"></div> 9 12 <a href="{% url 'register' %}" class="btn btn-sm btn-neutral">{% trans 'Register Account' %}</a> 10 13 </div> 11 14 <div class="bg-base-200 rounded p-8"> 15 + 16 + {% include 'partials/attendee_login.html' with general_login=True %} 17 + 12 18 <form method="post"> 13 19 {% csrf_token %} 14 20 ··· 40 46 </div> 41 47 </form> 42 48 {% if google_sso_enabled %} 43 - <div class="divider"></div> 49 + <div class="divider">Staff Login</div> 44 50 <a href="{{ google_sso_auth_url }}" class="btn w-full bg-[#4285F4] hover:bg-[#4285F4]/90 border-[#4285F4] text-white"> 45 51 <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 2a9.96 9.96 0 0 1 6.29 2.226a1 1 0 0 1 .04 1.52l-1.51 1.362a1 1 0 0 1 -1.265 .06a6 6 0 1 0 2.103 6.836l.001 -.004h-3.66a1 1 0 0 1 -.992 -.883l-.007 -.117v-2a1 1 0 0 1 1 -1h6.945a1 1 0 0 1 .994 .89c.04 .367 .061 .737 .061 1.11c0 5.523 -4.477 10 -10 10s-10 -4.477 -10 -10s4.477 -10 10 -10z" stroke-width="0" fill="currentColor" /></svg> 46 52 {% trans 'Log in with Google' %}
+2
paw/templates/ticketing/ticket_detail.html
··· 159 159 </div> 160 160 {% endif %} 161 161 162 + {% include 'partials/attendee_info.html' with fbl_account=ticket.user.fblaccount %} 163 + 162 164 <div class="divider"></div> 163 165 <h2 class="font-semibold mb-4">{% trans 'Category' %}</h2> 164 166 <div class="text-base-content/85 flex items-center text-sm mb-6">
+1
paw/urls.py
··· 26 26 path("", include("core.urls")), 27 27 path("", include("ticketing.urls")), 28 28 path("status", include("status.urls")), 29 + path("fbl/", include("fbl_integration.urls")), 29 30 ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 30 31 31 32 if settings.DEBUG:
+1
theme/tailwind.config.js
··· 2 2 module.exports = { 3 3 content: [ 4 4 "../paw/templates/**/*.{html,js}", 5 + "../fbl_integration/templates/**/*.{html,js}", 5 6 "../status/templates/**/*.{html,js}", 6 7 ], 7 8 theme: {