Code cleanup, readme written

This commit is contained in:
Ed 2023-07-29 16:05:46 +02:00
parent b1fb2bb105
commit d53ee2eaf0
54 changed files with 78 additions and 876 deletions

View File

14
app.py
View File

@ -29,12 +29,9 @@ from export import bp as export_bp
from stats import bp as stats_bp from stats import bp as stats_bp
from api import bp as api_bp from api import bp as api_bp
from carpooling import bp as carpooling_bp from carpooling import bp as carpooling_bp
from nfc import bp as nfc_bp
from checkin import bp as checkin_bp from checkin import bp as checkin_bp
from money import bp as money_bp
from boop import bp as boop_bp
app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, nfc_bp, checkin_bp, money_bp, boop_bp]) app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp])
@app.exception(exceptions.SanicException) @app.exception(exceptions.SanicException)
async def clear_session(request, exception): async def clear_session(request, exception):
@ -57,15 +54,9 @@ async def main_start(*_):
log.info("Cache fill done!") log.info("Cache fill done!")
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db') app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
app.ctx.boop = sqlite3.connect('data/boop.db')
app.ctx.money = sqlite3.connect('data/money.db')
app.ctx.money.row_factory = sqlite3.Row
app.ctx.login_codes = {} app.ctx.login_codes = {}
app.ctx.nfc_reads = {}
app.ctx.boops = Queue()
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True) app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
app.ctx.tpl.globals.update(time=time) app.ctx.tpl.globals.update(time=time)
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE) app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
@ -80,7 +71,7 @@ async def gen_barcode(request, code):
return raw(img.getvalue(), content_type="image/png") return raw(img.getvalue(), content_type="image/png")
@app.route("/furizon/beyond/order/<code>/<secret>/open/<secret2>") @app.route(f"/{ORGANIZER}/{EVENT_NAME}/order/<code>/<secret>/open/<secret2>")
async def redirect_explore(request, code, secret, order: Order, secret2=None): async def redirect_explore(request, code, secret, order: Order, secret2=None):
r = redirect(app.url_for("welcome")) r = redirect(app.url_for("welcome"))
@ -89,6 +80,7 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None):
if not order: if not order:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, f"orders/{code}/"), headers=headers) res = await client.get(join(base_url, f"orders/{code}/"), headers=headers)
print(res.json())
if res.status_code != 200: if res.status_code != 200:
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.") raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")

86
boop.py
View File

@ -1,86 +0,0 @@
from sanic import Blueprint, exceptions, response
from time import time
from asyncio import Future
import asyncio
from datetime import datetime
from asyncio import Queue
from random import randint
from boop_process import boop_process
bp = Blueprint("boop", url_prefix="/boop")
bp.ctx.boopbox_queue = {'a': Queue(), 'b': Queue(), 'c': Queue()}
bp.ctx.busy = {'a': False, 'b': False, 'c': False}
bp.ctx.last_tag = {'a': None, 'b': None, 'c': None}
bp.ctx.repeats = {'a': None, 'b': None, 'c': None}
def enable_nfc(enable, boopbox_id):
log.info('NFC is ' + ('enabled' if enable else 'disabled'))
app.ctx.queue.put_nowait({'_': 'nfc', 'enabled': enable})
app.ctx.nfc_enabled = enable;
@bp.get("/refresh")
async def refresh_boops(request):
await boop_process(request.app.ctx.om.cache.values(), request.app.ctx.boop)
return response.text('ok')
@bp.get("/")
async def show_boopbox(request):
tpl = request.app.ctx.tpl.get_template('boopbox.html')
return response.html(tpl.render())
@bp.get("/getqueue/<boopbox_id>")
async def boop_queue(request, boopbox_id):
items = []
queue = bp.ctx.boopbox_queue[boopbox_id]
while 1:
try:
item = queue.get_nowait()
except asyncio.queues.QueueEmpty:
if items:
break
# Try one last time to get a task, then fail.
bp.ctx.busy[boopbox_id] = False
try:
item = await asyncio.wait_for(queue.get(), timeout=5)
except asyncio.exceptions.TimeoutError:
break
items.append(item)
if len(items):
bp.ctx.busy[boopbox_id] = True
return response.json(items)
@bp.post("/read")
async def handle_boop(request):
payload = request.json
queue = bp.ctx.boopbox_queue[payload['boopbox_id']]
if bp.ctx.busy[payload['boopbox_id']]: return response.text('busy')
await queue.put({'_': 'play', 'src': f"/res/snd/error.wav"})
if bp.ctx.last_tag[payload['boopbox_id']] == payload['id']:
bp.ctx.repeats[payload['boopbox_id']] += 1
else:
bp.ctx.last_tag[payload['boopbox_id']] = payload['id']
bp.ctx.repeats[payload['boopbox_id']] = 0
if bp.ctx.repeats[payload['boopbox_id']] > 5:
await queue.put({'_': 'play', 'src': f"/res/snd/ratelimit.wav"})
await queue.put({'_': 'bye'})
return response.text('ok')
if bp.ctx.repeats[payload['boopbox_id']] > 10:
await queue.put({'_': 'talk', 'who': 'tiger', 'msg': f"Hey! Stop that! You're not the only one here!"})
await queue.put({'_': 'bye'})
return response.text('ok')
request.app.ctx.boop.execute('INSERT INTO boop(tag_id, station, ts) VALUES (?,?,?)', (payload['id'], payload['boopbox_id'], int(time())))
request.app.ctx.boop.commit()
return response.text('ok')

View File

@ -1,42 +0,0 @@
from datetime import datetime
from random import randint
from hashlib import md5
from base64 import b16encode
fibonacci = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233]
current_date = datetime.today()
async def boop_process(orders, db):
print(f'Processing {len(orders)} orders')
db.execute('DELETE FROM player')
for o in orders:
tags = []
code = o.code
tag_id = o.nfc_id or b16encode(md5(code.encode()).digest()).decode()[:14]
birthday = o.birth_date
badge_id = o.badge_id or randint(0,200)
room = o.actual_room or randint(100,400)
country = o.country
name = o.first_name.lower().strip()
birth_date = datetime.strptime(birthday, "%Y-%m-%d").date()
age = current_date.year - birth_date.year
if (current_date.month, current_date.day) < (birth_date.month, birth_date.day):
age -= 1
# Check if the birthday is today
if (current_date.month, current_date.day) == (birth_date.month, birth_date.day):
tags.append('birthday')
# Check if the badge is a fib number
if badge_id in fibonacci:
tags.append('fibonacci')
db.execute('INSERT INTO player(tag_id, code, tags, birthday, badge_id, age, name, room, country) VALUES (?,?,?,?,?,?,?,?,?)',
(tag_id, code, ','.join(tags), birthday, badge_id, age, name, room, country)
)
db.commit()

Binary file not shown.

BIN
data/boop.db Normal file

Binary file not shown.

Binary file not shown.

BIN
data/money.db Normal file

Binary file not shown.

BIN
data/nfc_counts.db Normal file

Binary file not shown.

21
ext.py
View File

@ -34,7 +34,7 @@ class Order:
self.checked_in = False self.checked_in = False
for p in self.data['positions']: for p in self.data['positions']:
if p['item'] in [16, 38]: if p['item'] in ITEM_IDS['ticket']:
self.position_id = p['id'] self.position_id = p['id']
self.position_positionid = p['positionid'] self.position_positionid = p['positionid']
self.answers = p['answers'] self.answers = p['answers']
@ -42,11 +42,11 @@ class Order:
self.address = f"{p['street']} - {p['zipcode']} {p['city']} - {p['country']}" self.address = f"{p['street']} - {p['zipcode']} {p['city']} - {p['country']}"
self.checked_in = bool(p['checkins']) self.checked_in = bool(p['checkins'])
if p['item'] == 17: if p['item'] in ITEM_IDS['membership_card']:
self.has_card = True self.has_card = True
if p['item'] == 19: if p['item'] in ITEM_IDS['sponsorship']:
self.sponsorship = 'normal' if p['variation'] == 13 else 'super' self.sponsorship = 'normal' if p['variation'] == ITEMS_IDS['sponsorship'][0] else 'super'
if p['country']: if p['country']:
self.country = p['country'] self.country = p['country']
@ -55,10 +55,10 @@ class Order:
self.first_name = p['attendee_name_parts']['given_name'] self.first_name = p['attendee_name_parts']['given_name']
self.last_name = p['attendee_name_parts']['family_name'] self.last_name = p['attendee_name_parts']['family_name']
if p['item'] == 20: if p['item'] == ITEM_IDS['early_arrival']:
self.has_early = True self.has_early = True
if p['item'] == 21: if p['item'] == ITEM_IDS['late_departure']:
self.has_late = True self.has_late = True
self.total = float(data['total']) self.total = float(data['total'])
@ -172,17 +172,10 @@ class Order:
class Quotas: class Quotas:
def __init__(self, data): def __init__(self, data):
self.data = data self.data = data
self.capacity_mapping = {
1: 16,
2: 17,
3: 18,
4: 19,
5: 20
}
def get_left(self, capacity): def get_left(self, capacity):
for quota in self.data['results']: for quota in self.data['results']:
if quota['id'] == self.capacity_mapping[capacity]: if quota['id'] == ROOM_MAP[capacity]:
return quota['available_number'] return quota['available_number']
async def get_quotas(request: Request=None): async def get_quotas(request: Request=None):

View File

@ -1,53 +0,0 @@
from sanic import Blueprint, exceptions, response
from time import time
from asyncio import Future
import asyncio
from datetime import datetime
bp = Blueprint("money", url_prefix="/money")
@bp.post("/pos")
async def do_transaction(request):
message = ''
tx_id = request.app.ctx.money.execute('INSERT INTO tx(tag_id, amount, ts) VALUES (?,?,?) RETURNING id', (request.form.get('nfc_id'), request.form.get('total'), time())).fetchone()[0]
for item, qty in request.form.items():
if not item.startswith('itm_'): continue
if qty[0] == '0': continue
request.app.ctx.money.execute('INSERT INTO tx_items(tx_id, item_id, qty) VALUES (?,?,?)', (tx_id, item[4:], qty[0]))
request.app.ctx.money.commit()
return await show_transactions(request, message='Transazione eseguita con successo!')
@bp.get("/pos")
async def show_transactions(request, message=None):
tpl = request.app.ctx.tpl.get_template('pos.html')
items = request.app.ctx.money.execute('SELECT * FROM item')
tx_info = {}
last_tx = request.app.ctx.money.execute('SELECT * FROM tx WHERE amount < 0 ORDER BY ts DESC LIMIT 3').fetchall()
for tx in last_tx:
tx_info[tx['id']] = {'items': request.app.ctx.money.execute('SELECT * FROM tx_items JOIN item ON item_id = item.id AND tx_id = ?', (tx['id'],)).fetchall(), 'order': await request.app.ctx.om.get_order(nfc_id=tx['tag_id']), 'time': datetime.fromtimestamp(tx['ts'] or 0).strftime('%H:%M')}
return response.html(tpl.render(items=items, message=message, last_tx=last_tx, tx_info=tx_info))
@bp.get("/poll_barcode")
async def give_barcode(request):
request.app.ctx.nfc_reads[request.ip] = Future()
try:
bcd = await asyncio.wait_for(request.app.ctx.nfc_reads[request.ip], 20)
except asyncio.TimeoutError:
if not request.ip in request.app.ctx.nfc_reads:
del request.app.ctx.nfc_reads[request.ip]
return response.json({'error': 'no_barcode'})
info = request.app.ctx.money.execute("SELECT count(*), coalesce(sum(amount), 0) FROM tx WHERE coalesce(is_canceled, 0) != 1 AND tag_id = ?", (bcd['id'],)).fetchone()
order = await request.app.ctx.om.get_order(nfc_id=bcd['id'])
desc = ("⚠️" if not bcd['is_secure'] else '') + (f"👤 {order.code} {order.name}" if order else f"🪙 {bcd['id']}") + f" · Transazioni: {info[0]}"
return response.json({**bcd, 'txamt': info[0], 'balance': info[1], 'desc': desc})

72
nfc.py
View File

@ -1,72 +0,0 @@
from sanic import Blueprint, exceptions, response
from random import choice
from ext import *
from config import headers, PROPIC_DEADLINE
from PIL import Image
from os.path import isfile
from os import unlink
from io import BytesIO
from hashlib import sha224
from time import time
from urllib.parse import unquote
from base64 import b16decode
from asyncio import Future
import json
import re
import asyncio
bp = Blueprint("nfc")
@bp.post("/nfc/read")
async def handle_reading(request):
payload = request.json
if payload['count']:
request.app.ctx.nfc_counts.execute('INSERT INTO read_count(tag_id, ts, count) VALUES (?,?,?)', (payload['id'], int(time()), payload['count']))
request.app.ctx.nfc_counts.commit()
if payload['boopbox_id']:
await request.app.ctx.boopbox_queue.put(payload)
return response.text('ok')
if not request.ip in request.app.ctx.nfc_reads:
return response.text('wasted read')
try:
request.app.ctx.nfc_reads[request.ip].set_result(payload)
except asyncio.exceptions.InvalidStateError:
del request.app.ctx.nfc_reads[request.ip]
return response.text('ok')
@bp.get("/fz23/<tag_id:([bc][0-9A-F]{14}x[0-9A-F]{6})>")
async def handle_nfc_tag(request, tag_id):
tag = re.match('([bc])([0-9A-F]{14})x([0-9A-F]{6})', tag_id)
# Store the read count
read_count = int.from_bytes(b16decode(tag.group(3)))
request.app.ctx.nfc_counts.execute('INSERT INTO read_count(tag_id, ts, count, is_web) VALUES (?,?,?,1)', (tag.group(2), int(time()), read_count))
request.app.ctx.nfc_counts.commit()
# If it's a coin, just show the coin template
if tag.group(1) == 'c':
return response.redirect(f"coin/{tag.group(2)}")
# If it's a badge, look for the corresponding order
for o in request.app.ctx.om.cache.values():
if o.nfc_id == tag.group(2):
return response.redirect(o.code)
raise exceptions.NotFound("Unknown tag :(")
@bp.get("/fz23/coin/<tag_id>")
async def show_coin(request, tag_id):
balance = request.app.ctx.money.execute('SELECT sum(amount) FROM tx WHERE tag_id = ?', (tag_id,)).fetchone()[0]
tpl = request.app.ctx.tpl.get_template('coin.html')
return response.html(tpl.render(balance=balance))
@bp.get("/fz23/<code:[A-Z0-9]{5}>")
async def show_order(request, code):
return response.html(f"<h1>Badge</h1><p>{code}</p>")

26
readme.md Normal file
View File

@ -0,0 +1,26 @@
# Furizon Webint
Furizon Webint is a powerful control panel designed to complement Pretix, providing management of various aspects related to the attendance of participants at furry conventions. Originally developed for Furizon Beyond (2023), this application is currently undergoing a rehaul to become more versatile and adaptable for use in any convention.
## How does it work?
The integration with Pretix is achieved by leveraging a simple nginx rule. When individuals place orders through Pretix, they usually receive a "magic" link that allows them to manage their order. Using a nginx rule, we redirect these requests to this backend. This process is seamless because the essential information needed for managing Pretix orders can still be accessed via a shorter URL without compromising any functionality.
## Why not a pretix plugin?
Developing plugins for Pretix was far too tedious, and Pretix didn't have the flexibility needed for this panel.
## What can it do?
- User badges management (allow attendees to upload pictures within the deadlines)
- Manage hotel rooms (attendees can create, join, delete rooms)
- Show a nosecount public page
- Data export
- Car pooling (let attendees post announcements and organize trips)
- Karaoke Queue management (apply to sing for the karaoke contest and manage the queue)
- Manage the events and present them via API for usage with he app
- Export an API to be used for the mobile app (no plans to open source that, sorry ☹️)
- Check-in management
## How to run it
1. Create a Python virtual environment (venv).
2. Install the required dependencies from the `requirements.txt` file.
3. Edit the `config.py` file with your specific data. You can use `config.example.py` as a template to guide you.
4. Set up an nginx rule to redirect requests for `/manage/` and `/[a-z0-9]+/[a-z0-9]+/order/[A-Z0-9]+/[a-z0-9]+/open/[a-z0-9]+/` to the Furizon Webint backend.
5. Run `app.py`. By default, the application will listen on `0.0.0.0:8188`.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -279,8 +279,8 @@ async def confirm_room(request, order: Order, quotas: Quotas):
thing = { thing = {
'order': order.code, 'order': order.code,
'addon_to': order.position_positionid, 'addon_to': order.position_positionid,
'item': 39, 'item': ITEM_IDS['room'],
'variation': ([None, 16, 17, 18, 19, 20])[len(room_members)] 'variation': ROOM_MAP[len(room_members)]
} }
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -294,8 +294,8 @@ async def confirm_room(request, order: Order, quotas: Quotas):
thing = { thing = {
'order': rm.code, 'order': rm.code,
'addon_to': rm.position_positionid, 'addon_to': rm.position_positionid,
'item': 42, 'item': ITEM_IDS['room'],
'variation': ([None, 21, 22, 23, 24, 25])[len(room_members)] 'variation': ROOM_MAP[len(room_members)]
} }
res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing) res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing)

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<head>
<meta charset="utf8" />
<title>{{order.name}}</title>
<meta name="viewport" content="width=380rem" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Lato&display=swap');
* {border:0;margin:0;padding:0;}
body{background:#000;color:#ddd;font-family:'Lato';font-size:1.1rem;}
h1 {font-size: 3.5em;text-transform:uppercase}
#center {margin:1em auto;padding:0 1rem;max-width:30rem;text-align:center;}
nav > a {font-size:1.1em;display:block;background-color:#336;color:#fff;text-decoration:none;padding:0.5em;border-radius:3em;text-align:center;margin:1em;font-weight:bold;}
a > img {display:inline;height:1em;line-height:1em;vertical-align:middle;}
p {margin:0.6em 0;}
hr {border:0;height:12px;background:url('wiggly.png');background-repeat:no-repeat;background-position:center;margin:1.5em 0;}
a {color:#f0f;}
.bebas {font-family:'Bebas Neue';font-weight:900;background: linear-gradient(to right, #f32170, #ff6b08, #cf23cf, #eedd44);
-webkit-text-fill-color: transparent;-webkit-background-clip: text;transform:.3s all;}
</style>
</head>
<body>
<div id="center">
<img src="https://reg.furizon.net/res/propic/{{order.propic or 'default.png'}}" style="width:100%;max-height:10em;max-width:10em;"/>
<hr />
<h2 class="bebas" style="font-size:4em;font-family:'Bebas Neue';">{{order.name}}</h2>
<p>Is an attendee of Furizon 2023 :D<br/><br />
<hr />
<nav>
<a style="background:#1086D7;" href="https://t.me/{{order.telegram_username}}">Telegram</a>
</nav>
<h3 class="bebas">Stay cool~<br/>Stay Furizon</h3>
</div>
</body>
</html>

View File

@ -1,85 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="/res/boopbox.css" />
</head>
<body>
<div id="commands">
<a href="javascript:document.documentElement.requestFullscreen();">Fs</a>
<a href="javascript:document.getElementById('wolf').setAttribute('disabled', 'true');">Wolf hide</a>
<a href="javascript:document.getElementById('wolf').removeAttribute('disabled');">Wolf show</a>
<a href="javascript:document.getElementById('tiger').setAttribute('disabled', 'true');">Tiger hide</a>
<a href="javascript:document.getElementById('tiger').removeAttribute('disabled');">Tiger show</a>
<a href="javascript:document.getElementById('msgbox').removeAttribute('disabled');">Chatbox show</a>
<a href="javascript:document.getElementById('msgbox').setAttribute('disabled', 'true');">Chatbox hide</a>
<span id="debug">Debug command</span>
</div>
<!--<div id="error">
<p>An unrecoverable error has happened. Please call foxo.</p>
</div>-->
<!--<div id="border"></div>-->
<div id="main">
<img class="char bye" id="wolf" src="/res/wolf.png" disabled="true" />
<img class="char bye" id="tiger" src="/res/tiger.png" disabled="true" />
<div id="msgbox" class="bye" disabled><span id="msgcontent"></span><span id="touch" disabled>(touch the screen to continue <img src="/res/touch.svg" />)</span></div>
<div id="events" class="welcome-back">
<div id="eventsbox">
<h1 style="margin-top:7em;display:none;" id="nothing-happening">It looks like nothing is happening now~<br />_-¯¯-¯_<br />See you later :)</h1>
</div>
<h1>See more events with our app!</h1>
</div>
<video id="tv" autoplay muted playsinline controls="false" poster="/res/tv.jpg" class="welcome-back">
<!-- <source id="video-source" src="http://192.168.32.189:8000/tron.webm" type="video/mp4"> -->
</video>
<script>
var video = document.getElementById('tv');
var source = document.getElementById('video-source');
video.controls = false; // Ensure controls are hidden
video.addEventListener('error', function() {
video.style.display = 'none';
});
function playVideo() {
var cachebuster = Date.now(); // Generate a unique timestamp
var videoUrl = 'http://192.168.32.189:8000/tron.webm?cachebuster=' + cachebuster;
source.src = videoUrl;
video.load();
video.play().catch(function() {
fallbackImage.style.display = 'block'; // Show fallback image on playback failure
});
}
playVideo();
</script>
<div id="column" class="welcome-back">
<h1>Useful Info</h1>
<h2>CALL SECURITY</h2>
<p>Dial (+39) 375 732 4734</p>
<hr />
<h2>Reception Open</h2>
<p>Every day 8:00~19:00</p>
<hr />
<h2>NFC issues?</h2>
<p>Search for Foxo (Badge ID 3) or write to @unfoxo on Telegram</p>
<hr />
<h2 id="clock"></h2>
</div>
<div id="nfcstat" class="welcome-back" disabled>Dealer's Den <img src="/res/icons/nfc.svg" /></div>
</div>
<script type="text/javascript" src="/res/boopbox.js"></script>
</body>
</html>

View File

@ -1,44 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>You found a coin!</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
@import url(https://fonts.bunny.net/css?family=arbutus:400);
body {font-family:'arbutus', 'Comic Sans MS', sans-serif;background:url('/res/wood.jpg') #222 no-repeat;background-size:cover;color:#eee;text-align:center;}
strong {color:#de0;}
main {display:block;max-width:26em;margin:1em auto;}
video {display:block;width:100%;}
</style>
</head>
<body>
<main>
<video autoplay muted oncanplay="preloadSecondVideo()" onended="loopVideo()">
<source src="/res/video/coin-intro.webm" type="video/webm">
<img src="/res/video/coin.webp" alt="Fallback Image">
</video>
{% if balance < 0 %}
<h1>You found a<br/><strong>{{balance}}FZ</strong><br />coin!</h1>
{% else %}
<h1>You found a coin!<br />(but sadly, it's already spent)</h1>
{% endif %}
</main>
<script>
function preloadSecondVideo() {
var secondVideo = new Image(); // Create an image element to preload the second video
secondVideo.src = '/res/video/coin-loop.webm';
}
function loopVideo() {
var video = document.querySelector('video');
video.src = '/res/video/coin-loop.webm';
video.loop = true;
video.load();
video.play();
}
</script>
</body>
</html>

View File

@ -1,97 +0,0 @@
{% extends "base.html" %}
{% block title %}Your Booking{% endblock %}
{% block main %}
<main class="container">
<header>
<h1><img src="{{order.ans('propic') or '/res/avatar.jpg'}}" class="propic"> {{order.ans('fursona_name')}}'s booking</h1>
</header>
{% if order.status() == 'pending' %}
<p class="notice">⚠️ Your order is still pending. You will not be able to reserve a room. <a href="#">Check your payment status</a></p>
{% endif %}
{% if not order.ans('propic') %}
<p class="notice">⚠️ You still haven't uploaded a profile pic. <a href="#">Upload one now</a></p>
{% endif %}
<section id="room_prompt">
<h3>Room Status:
{% if order.ans('room_confirmed') %}
{{order.ans('room_confirmed')}}
<span style="color: green;">room confirmed</span></h3>
<p>Your room is confirmed! Enjoy the convention :)</p>
{% elif order.ans('room_members') %}
<span style="color: yellow;">pending room</span></h3>
<p>You have a room, but it is not confirmed! Make sure all room owners have paid, then use the button below to reserve the room for two people.</p>
<h5>Your room</h5>
<div class="grid people">
{% set room = namespace(forbidden=false) %}
{% for person in roommate_orders %}
<div>
<img class="propic" src="https://picsum.photos/200" />
<h3>{{person.ans('fursona_name')}}</h3>
<p>{{person.ans('staff_title') if person.ans('staff_title') else ''}}{{' · Fursuiter' if person.ans('is_fursuiter') != 'No'}}</p>
<p>{{('<strong style="color:red;">UNPAID</strong>' if person.status() == 'pending')|safe}}</p>
</div>
{% if person.status() != 'paid' %}
{% set room.forbidden = True %}
{% endif %}
{% endfor %}
</div>
<h5>Available rooms</h5>
<table>
<tr>
<td>Single room</td>
<td>{{room_qty[1]}} available</td>
</tr>
<tr>
<td>Double room</td>
<td>{{room_qty[2]}} available</td>
</tr>
<tr>
<td>Triple room</td>
<td>{{room_qty[3]}} available</td>
</tr>
<tr>
<td>Quadruple room</td>
<td>{{room_qty[4]}} available</td>
</tr>
<tr>
<td>Quintuple room</td>
<td>{{room_qty[5]}} available</td>
</tr>
</table>
{% if room.forbidden %}
<p>Since at least one of your roommates still have a pending reservation, you will not be able to reserve a room for now.</p>
<button disabled>Reserve a room for 3 people</button>
{% else %}
<button>Reserve a room for 3 people</button>
{% endif %}
{% else %}
<span style="color: red;">no room</span></h3>
<p>You currently don't have any room. If you don't create or join one within 42 days, 10 hours, 32 minutes, 13 seconds, we will assign you to a random one.</p>
<form>
<div class="grid">
<button>Create a room</button>
<button>Join another room</button>
</div>
</form>
{% endif %}
</section>
</main>
<!--<section id="room_prompt">
<h3>Room status: </h3>
<p>Good news</p>
<div class="grid">
<button>Create a room</button>
<button>Join another room</button>
</div>
</section>
</main>-->
{% endblock %}

View File

@ -29,12 +29,12 @@
</div> </div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed and len(o.room_members) > 1) %}
{% if loop.first %}
<hr /> <hr />
<h1>Unconfirmed rooms</h1> <h1>Unconfirmed rooms</h1>
<p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p> <p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p>
{% for o in orders.values() %} {% endif %}
{% if o.code == o.room_id and not o.room_confirmed and len(o.room_members) > 1 %}
<h3>{% if o.room_confirmed %}🔒{% endif %}{{o.room_name}}</h3> <h3>{% if o.room_confirmed %}🔒{% endif %}{{o.room_name}}</h3>
<div class="grid people" style="padding-bottom:1em;"> <div class="grid people" style="padding-bottom:1em;">
{% for m in o.room_members %} {% for m in o.room_members %}
@ -51,15 +51,16 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% if loop.last %}</div>{% endif %}
{% endfor %} {% endfor %}
{% for person in orders.values() if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed)%}
{% if loop.first %}
<hr /> <hr />
<h1>Roomless furs</h1> <h1>Roomless furs</h1>
<p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p> <p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p>
<div class="grid people" style="padding-bottom:1em;"> <div class="grid people" style="padding-bottom:1em;">
{% for person in orders.values() %} {% endif %}
{% if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed) %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
<div class="propic-container"> <div class="propic-container">
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" /> <img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
@ -67,8 +68,8 @@
</div> </div>
<h5>{{person.ans('fursona_name')}}</h5> <h5>{{person.ans('fursona_name')}}</h5>
</div> </div>
{% endif %} {% if loop.last %}</div>{% endif %}
{% endfor %} {% endfor %}
</div>
</main> </main>
{% endblock %} {% endblock %}

View File

@ -1,294 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Fz vending</title>
<style>
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
@keyframes shake {
0%, 100% {
transform: translateX(0);
}
25%, 75% {
transform: translateX(-10px);
}
50% {
transform: translateX(10px);
}
}
body {margin:0;padding:0;background:#222;color:#eee;font-family:"Open Sans", monospace;}
header {padding-left:0.5em;height:4rem;line-height:4rem;background:#111;font-size:2.2rem;color:#fa0;}
header img {height:3.6rem;margin-right:0.3em;display:inline-block;vertical-align:middle;}
header strong {font-size:1.05em;color:#f00;vertical-align:baseline;}
aside {position:fixed;height:100%;overflow:auto;top:0;width:18em;right:0;background:#111;}
main {padding-right:18em;}
.shake {animation: shake 0.3s 1;}
#toolkit {position:fixed;bottom:0;right:0;width:18em;padding:0.5em;box-sizing:border-box;background:#000;text-align:center;}
#toolkit img {display:inline;height:3rem;margin:0.5rem 0.1rem;border:1px dashed rgba(255,255,255,0.2);padding:0.2em;}
h2 {margin-top:0;font-size: 2.2rem;line-height:4rem;display:block;text-align:center;background:#070;color:#eee;}
h3 {margin-top:1em;font-size: 1.4rem;display:block;text-align:center;padding:0.3em 0em;background:#fa0;color:#000;}
button {background: #fa0;padding:0.2em;margin:0.1em;font-size:1.2em;width:1.6em;border-radius:6px;border:none;transition:all 0.1s;}
button:active {transform: scale(0.95);}
aside strong {font-size:1.9em;color:#f00;vertical-align:baseline;}
aside button {font-size:3em;}
ul {padding:0.5em 1em;}
li {line-height:1.8em;}
p {margin-bottom: 1em;}
em {font-size: 1.3em;font-weight:bold;}
sup {font-size: 0.7em;color:#fc0;}
table {width:100%;font-size:1.3em;}
table td {padding:0.1em;}
table td:first-child {color:#0f0;text-align:right;}
#mask {position:fixed;height:100%;width:100%;top:0;left:0;background:rgba(0,0,0,0.3);z-index:9999;backdrop-filter: blur(20px);text-align:center;}
#mask h1 {font-size:4em;margin:3em 0 0.6em;}
#mask button {font-size:3em;background:#ddd;}
.secret {display:none;}
input {color:#eee;background:#000;font-size:2em;width:2em;font-size:center;border:none;text-align:center;}
</style>
</head>
<body>
<header id="nfcInfo">{{message or 'Scannerizza un NFC...'}}</header>
<form method="post" action="pos">
<input type="hidden" id="totalValue" name="total" />
<input type="hidden" id="nfcid" name="nfc_id" value="" />
<main>
<table>
{% for item in items %}
<tr {% if item.price > 0 %}class="secret"{% endif %}>
<td>{{item.name}}</td>
<td><strong>{{item.price}}FZ</strong></td>
<td>
<button type="button" onclick="decrementValue(this)">-</button>
<input type="text" name="itm_{{item.id}}" value="0" data-price="{{item.price}}" autocomplete="off" oninput="updateTotal();" />
<button type="button" onclick="incrementValue(this)">+</button>
</td>
</tr>
{% endfor %}
</table>
</main>
<aside>
<h2><span id="badgeBalance">0</span> FZ</h2>
<p style="text-align:center;">Totale <strong><span id="totalValueShow">0</span>FZ</strong></p>
<div style="text-align:center;"><button style="background:#090;" id="sendButton" disabled="disabled"></button><button type="button" onClick="zeroItems();" style="background:#c00;"></button></div>
<h3 id="triggerElement">Ultime transazioni</h3>
{% for tx in last_tx %}
<ul>
<li><em>{{tx_info[tx['id']]['order'].name or tx['tag_id']}}<sup>{{tx_info[tx['id']]['time']}}</sup></em></li>
{% for tx_item in tx_info[tx['id']]['items'] %}
<li><strong>{{tx_item['qty']}}x</strong> {{tx_item['name']}}</li>
{% endfor %}
<li><em>Totale: {{tx['amount']}}FZ</em></li>
</ul>
{% endfor %}
<span style="display:none;" id="debug"></span>
</aside>
<div id="toolkit">
<a href="pos"><img src="/res/icons/refresh.svg" style="filter: invert(1);"/></a>
<a href="#"><img src="/res/icons/qr.svg" id="executeButton" style="filter: invert(1);"/></a>
</div>
</form>
</body>
<script>
let isClosing = false;
window.addEventListener('beforeunload', handleBeforeUnload);
function handleBeforeUnload(event) {
isClosing = true;
}
const triggerElement = document.getElementById('triggerElement');
const secretElements = document.querySelectorAll('.secret');
let secretElementsVisible = localStorage.getItem('secretElementsVisible');
let pressStart;
let isPress = false;
triggerElement.addEventListener('mousedown', handleMouseDown);
triggerElement.addEventListener('touchstart', handleMouseDown);
triggerElement.addEventListener('mouseup', handleMouseUp);
triggerElement.addEventListener('touchend', handleMouseUp);
function handleMouseDown() {
pressStart = Date.now();
isPress = true;
setTimeout(toggleSecretElements, 1200); // Delay the toggle action by 500 milliseconds
}
function handleMouseUp() {
isPress = false;
}
if (secretElementsVisible === 'true') {
secretElements.forEach(element => {
element.classList.remove('secret');
});
}
const executeButton = document.getElementById('executeButton');
executeButton.addEventListener('click', () => {
const code = prompt('Enter the code:');
if (code && /^([A-F0-9]{2}){4,}$/.test(code)) {
const payload = {
id: code,
is_secure: false,
count: null,
boopbox_id: null
};
fetch('/nfc/read', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
// Handle the response data as needed
console.log(data);
})
.catch(error => {
// Handle any errors that occur during the POST request
console.error(error);
});
} else if (code !== null) {
alert('Invalid code format. Please enter a valid code.');
}
});
function toggleSecretElements() {
const pressDuration = Date.now() - pressStart;
if (pressDuration >= 1000 && isPress) {
secretElementsVisible = !secretElementsVisible;
if (secretElementsVisible) {
localStorage.setItem('secretElementsVisible', 'true');
secretElements.forEach(element => {
element.classList.remove('secret');
});
} else {
localStorage.removeItem('secretElementsVisible');
secretElements.forEach(element => {
element.classList.add('secret');
});
}
}
}
// Function to perform long-polling
function longPoll() {
fetch('poll_barcode')
.then((response) => response.json())
.then((data) => {
// Update the table with received JSON data
if(!data.error) {
document.getElementById('nfcInfo').classList.remove('shake');
document.getElementById('nfcInfo').innerHTML = data.desc;
void document.getElementById('nfcInfo').offsetWidth;
document.getElementById('nfcInfo').classList.add('shake');
document.getElementById('nfcid').value = data.id;
document.getElementById('badgeBalance').innerHTML = data.balance;
document.getElementById('debug').innerHTML = JSON.stringify(data);
} else {
console.log("Got no barcode.")
}
// Start the next long-poll request
updateTotal();
longPoll();
})
.catch((error) => {
if(isClosing) return;
document.getElementById('nfcInfo').innerHTML = error;
// Retry long-polling after a delay in case of errors
setTimeout(longPoll, 1000);
});
}
// Start long-polling
longPoll();
function incrementValue(button) {
var input = button.previousElementSibling;
var value = parseInt(input.value) || 0;
input.value = Math.max(value + 1, 0);
updateTotal();
}
function decrementValue(button) {
var input = button.nextElementSibling;
var value = parseInt(input.value) || 0;
input.value = Math.max(value - 1, 0);
updateTotal();
}
function zeroItems() {
var inputs = document.querySelectorAll('input[name^="itm_"]');
inputs.forEach(function(input) {
input.value = 0;
});
updateTotal();
}
function updateTotal() {
var inputs = document.querySelectorAll('input[name^="itm_"]');
var total = 0;
inputs.forEach(function(input) {
var value = parseInt(input.value) || 0;
var price = parseFloat(input.getAttribute('data-price'));
total += value * price;
});
document.getElementById('totalValue').value = total;
document.getElementById('totalValueShow').innerHTML = total;
let badgeBalance = Number(document.getElementById('badgeBalance').innerHTML);
if(badgeBalance+total < 0 || total == 0) {
document.getElementById('sendButton').setAttribute("disabled", "disabled");
} else {
document.getElementById('sendButton').removeAttribute("disabled");
}
}
</script>
</html>