Compare commits

...

109 Commits

Author SHA1 Message Date
Andrea 77c102c1cf wip9 - submit algorithm + client fixes
Refined the submit algorithm with more owner fixes, plus a final cache update

Handled a case in which a user by moving the generated room would keep the moved user as the owner of the room, resulting in the user to be in two rooms at the same time
2024-05-20 00:35:25 +02:00
Andrea e34af503ff wip 8 - Configured modal for room wizard submission, added room quota management, fixed error page not passing status code 2024-05-19 21:07:31 +02:00
Andrea f3a76b6e93 wip - handled excess rooms case for autofill 2024-05-19 11:03:48 +02:00
Andrea a0bcf3c046 wip - UI Update + Client model management + wip new software requirement in the backend matching algorithm 2024-05-19 00:06:36 +02:00
Andrea d9a792aef0 wip - client adjustments for chrome 2024-05-18 18:24:06 +02:00
Andrea 41373dfcfa wip5 - Users room re-arrangement + config fix
[+] Added the Drag'n'Drop mechanism in the wizard page, allowing to move orders between rooms, plus a 'placeholder' space to temporarily keep orders to move others

[minor fix] Item category maps might be filled twice with the same IDs
2024-05-16 23:52:09 +02:00
Andrea Carulli 13e5217601 wip - wizard review page 2024-05-15 18:02:38 +02:00
Andrea b968a78aa5 wip3 - Room wizard for autofilling unfilled rooms
Next wip will consist of filling up new rooms for roomless orders
2024-05-14 23:31:31 +02:00
Andrea Carulli 11fd547250 wip2 - new room assigning wizard 2024-05-14 18:03:19 +02:00
Andrea 63c0bb75db room autofill wip
Added a button in the nosecount
[Might be removed] a new dialog
2024-05-14 00:25:43 +02:00
drew ab2c8d4b85 Merge pull request 'Improved CSV export' (#26) from stranck-dev into drew-dev
Reviewed-on: #26
2024-05-13 20:10:58 +00:00
Stranck df4f2eaf81 Improved CSV export 2024-05-13 13:30:21 +02:00
drew cd2a5405db Merge pull request 'stranck-dev' (#25) from stranck-dev into drew-dev
Reviewed-on: #25
2024-05-13 10:17:12 +00:00
Stranck 84bc070593 Added auto-confirm rooms to admin panel
Fucking untested hopefully it works
2024-05-13 12:01:47 +02:00
Stranck 1e6b400b2c Automatically remove canceled orders from rooms
Quite shit code, but it works

(I haven't slept last night)
2024-05-13 10:26:29 +02:00
Stranck 383b5bbede Better handling of canceled orders
If an order is canceled with a paid fee pretix still returns that it's paid
2024-05-13 10:25:45 +02:00
Stranck 938bc68383 Added extra log in case of generic errors
in update answers
2024-05-10 21:51:14 +02:00
Stranck 0d6789d307 Removed shuttle bus 2024-05-10 21:50:51 +02:00
drew db45c2e7e3 Merge pull request 'Fix daily people not showing up in the webint' (#24) from stranck-dev into drew-dev
Reviewed-on: #24
2024-05-07 19:32:29 +00:00
Stranck bc366c85e5 Fix daily people not showing up in the webint 2024-04-14 11:35:41 +02:00
drew 687eaf7317 Merge pull request 'stranck-dev' (#23) from stranck-dev into drew-dev
Reviewed-on: #23
2024-04-05 15:11:16 +00:00
Luca Sorace "Stranck d974870963 Fixed various bugs
The unconfirm rooms incident....
2024-03-20 17:28:35 +01:00
Luca Sorace "Stranck f4ebc83a9b Fixed problems with canceled orders 2024-03-12 21:35:52 +01:00
drew 76f0eee841 Merge pull request 'stranck-dev' (#22) from stranck-dev into drew-dev
Reviewed-on: #22
2024-03-05 09:07:54 +00:00
stranck c405a49366 Merge pull request 'drew-dev' (#21) from drew-dev into stranck-dev
Reviewed-on: #21
2024-03-04 09:36:41 +00:00
Andrea cb3a501280 [] Added confirm modal for badge reminder email feature 2024-03-02 17:06:00 +01:00
drew edc306c6c1 Merge pull request 'stranck-dev' (#20) from stranck-dev into drew-dev
Reviewed-on: #20
2024-03-02 15:34:55 +00:00
Luca Sorace "Stranck b1a67e74e3 Fixed file upload after pretixClient introduction 2024-03-02 13:44:49 +01:00
Stranck f233fb0b68 Update room.html 2024-02-29 14:57:46 +01:00
Stranck 99b8c87e5c Merge branch 'stranck-dev' of https://git.foxo.me/Furizon/furizon_webint into stranck-dev 2024-02-29 11:42:43 +01:00
Stranck 9f191c3dec Now smpt client will shutdown with SIGINTS 2024-02-29 11:42:05 +01:00
Stranck c6bc9c65ac Added remind propic feature for admins 2024-02-29 11:41:49 +01:00
Stranck 5b66a58399 Included grafana and redis folders in backups 2024-02-29 10:10:05 +01:00
Luca Sorace "Stranck 3919d512a1 Added metrics for early/late orders 2024-02-27 19:53:07 +01:00
Stranck 5d79052e59 Added rclone scripts 2024-02-26 16:06:44 +01:00
Stranck 1d7ea375c1 Better handling of pretix's unresponsiviness 2024-02-22 19:15:33 +01:00
Stranck 968f5f09ed Added back rainbow animation to sponsorcount text 2024-02-22 15:53:06 +01:00
Stranck 4e1120a93f Dumb fix for horizontal scrolling 2024-02-22 11:25:55 +01:00
stranck 44b534b283 Merge pull request 'drew-dev' (#19) from drew-dev into stranck-dev
Reviewed-on: #19
2024-02-22 10:13:24 +00:00
Andrea f8e58fd35d [wip1] nosecount filters 2024-02-19 07:55:23 +01:00
Andrea ac640c4185 [fix] re-added navigation script 2024-02-18 11:09:11 +01:00
drew 758cf4e3a6 Merge pull request 'stranck-dev' (#18) from stranck-dev into drew-dev
Reviewed-on: #18
2024-02-17 22:50:03 +00:00
Luca Sorace "Stranck 8d314d2a63 Removed foxo's tracker 2024-02-14 22:00:59 +01:00
Luca Sorace "Stranck 61447ee768 Added custom metrics
And removed old library
2024-02-14 20:22:56 +01:00
Luca Sorace "Stranck cfefd44435 moved logOrders in stuff/ 2024-02-14 16:42:21 +01:00
Luca Sorace "Stranck f59c288908 Added metrics 2024-02-14 16:10:27 +01:00
drew 78f677d19d Merge pull request 'stranck-dev in drew-dev' (#17) from stranck-dev into drew-dev
Reviewed-on: #17
2024-02-13 15:47:37 +00:00
Luca Sorace "Stranck 878719260d Added healtchecks on startup 2024-02-13 14:17:21 +01:00
Luca Sorace "Stranck 7574a9d356 Server migration 2024-02-13 13:29:03 +01:00
Luca Sorace "Stranck 165c9d6bb5 Migrated SMPT server to office365
+ image email fix
2024-02-11 23:57:52 +01:00
Luca Sorace "Stranck 0d663c5b58 Shuttle text is in prod :D 2024-02-10 18:44:38 +01:00
Luca Sorace "Stranck 6494a06ca8 Updated unconfirm rooms conditions 2024-02-10 18:44:20 +01:00
Luca Sorace "Stranck 951e4c0015 Fixed python compatibility 2024-02-09 18:04:50 +01:00
Luca Sorace "Stranck 5d851acbeb Update .gitignore 2024-02-09 16:12:42 +01:00
stranck e780a2a0c0 Merge pull request 'We're so back' (#16) from drew-dev into stranck-dev
Reviewed-on: #16
2024-02-09 15:09:26 +00:00
Andrea 1da054c9ff [WIP1] Localization, Shuttle bus section 2024-01-30 00:35:56 +01:00
Andrea a2b9f7a7c1 Simple cache debouncer, better logging, multi-room check optimizations
Added a simple cache refresh debouncer
Made the update_cache method return a boolean if the cache needed to update
Moved logging info to Sanic.log.logger
Made the admin check into a middleware
Added room check as a standalone action in the admin panel
Added a counter in the nose count for admins
2024-01-22 00:35:44 +01:00
Andrea Carulli a154c3059b [wip6] EMail sending re-ordering 2024-01-19 18:03:54 +01:00
Andrea 16ae51fb08 [wip5] Client error display 2024-01-19 01:15:20 +01:00
Andrea Carulli aa54032492 [wip4] Room preview generation improvements + Room checking with user email 2024-01-18 18:03:32 +01:00
Andrea 6fe87b6ea1 [wip3] Room preview generation + OpenGraph tags 2024-01-18 01:03:48 +01:00
Andrea Carulli 123a1dc3c5 [wip] Room preview generation 2024-01-17 18:03:38 +01:00
Andrea 7299bdb840 [wip] Room preview + Room get logic 2024-01-17 00:51:50 +01:00
drew 3878076e0f Merge pull request 'stranck-dev' (#15) from stranck-dev into drew-dev
Reviewed-on: #15
2024-01-16 13:33:28 +00:00
Stranck aed8bf41b7 Fixed full logout after loginAs 2024-01-13 23:39:15 +01:00
stranck 59c2a7d926 Merge pull request 'Added navbar' (#14) from drew-dev into stranck-dev
Reviewed-on: #14
2024-01-13 22:25:56 +00:00
Andrea 2662ece8ab Navbar updates 2024-01-13 23:24:22 +01:00
drew c9a7e5da8a Merge pull request 'Merge pull request 'Edits' (#12) from drew-dev into stranck-dev' (#13) from stranck-dev into drew-dev
Reviewed-on: #13
2024-01-13 22:22:29 +00:00
stranck c5d685b42b Merge pull request 'Edits' (#12) from drew-dev into stranck-dev
Reviewed-on: #12
2024-01-13 18:52:01 +00:00
Andrea eabfc2d5b0 Reduced effects for mobile users + Enter as UI for Admins 2024-01-13 19:45:32 +01:00
drew 957ebb5899 Merge pull request 'stranck-dev' (#10) from stranck-dev into drew-dev
Reviewed-on: #10
2024-01-13 16:02:42 +00:00
Stranck 8b07fa55b7 Added admin/loginas method 2024-01-13 16:59:24 +01:00
Stranck ca2ad6589b Fixed upload size for propics 2024-01-13 16:59:13 +01:00
Stranck c82d075913 Fixed room owner bug in /delete 2024-01-13 14:58:06 +01:00
Stranck 0af0849f13 Created tool to monitor new orders 2024-01-13 13:13:22 +01:00
Stranck 274dcbb3a3 Updated month in homepage 2024-01-13 13:12:55 +01:00
Stranck f3eb905298 Updated footer credits 2024-01-11 18:44:48 +01:00
Stranck dafe98dfbb Updated prints 2024-01-11 18:15:56 +01:00
Stranck 456361c585 Visual fixes with buttons 2024-01-10 14:04:04 +01:00
Stranck c5fb4fd55d Hash is not used anymore in propics upload 2024-01-10 11:24:24 +01:00
Stranck 9dfacc3b5b Fixed strong text in room button 2024-01-09 23:48:38 +01:00
Stranck 04e7bd3005 Fixed room button centering 2024-01-09 23:43:53 +01:00
Stranck 5c44c620e4 Updated sponsor animation speed + fixed bug on iOs 2024-01-09 23:43:39 +01:00
Stranck 5d815defc8 Fixed default propic and propic file removal 2024-01-09 23:42:54 +01:00
stranck 95525d28a2 Merge pull request 'Fix attempt propic animation on iPhones' (#9) from drew-dev into stranck-dev
Reviewed-on: #9
2024-01-09 09:17:41 +00:00
Andrea ffda843d45 [fix attempt] profile picture caching spacing issue 2024-01-09 00:23:21 +01:00
drew 8496c89977 Merge pull request 'stranck-dev' (#8) from stranck-dev into drew-dev
Reviewed-on: #8
2024-01-08 23:09:45 +00:00
Stranck d08cded007 Update getOrderByCode_safe method name 2024-01-08 23:16:26 +01:00
stranck 76c8df9e03 Merge pull request '[Various fixes]' (#7) from drew-dev into stranck-dev
Reviewed-on: #7
2024-01-08 22:08:59 +00:00
Stranck c7159cf3b4 Default for room renaming 2024-01-08 23:07:59 +01:00
Stranck fd3b0e727c Fixes 2024-01-08 23:05:35 +01:00
Stranck bebc7011cf Fixed python version support - propic 2024-01-08 23:04:34 +01:00
Andrea 823aaf4f13 [Various fixes]
Fix nosecount requiring an order to be displayed

Fix answer uploading bug

Code cleanup in propic
2024-01-08 23:04:11 +01:00
stranck f8ed348d51 Merge pull request 'We did a lot of stuff :D' (#6) from drew-dev into stranck-dev
Reviewed-on: #6
2024-01-08 21:04:40 +00:00
Andrea 53232de597 Auto Id Indexing + Admin panel + Propic upload checks + Room editing
Added auto indexing by metadata, instead of ID matching
Added checks to propic image uploading
Added an admin page, featuring:
 - Manual cache clearing
 Added admin controls in nosecount to unconfirm / rename or delete the room

Co-authored-by: Luca Sorace "Stranck" <stranck@users.noreply.github.com>
2024-01-08 22:02:27 +01:00
drew 01da35560f Merge pull request 'stranck-dev' (#5) from stranck-dev into drew-dev
Reviewed-on: #5
2024-01-07 20:31:00 +00:00
Stranck 25ed127e81 Fixed funny link in room.html 2024-01-06 12:58:23 +01:00
Stranck 6b70d91201 Fixed propic upload limits
If the size is bigger than 5MB the error will be handled by SANIC directly. Maybe we'd like a better user-visible handling?
2024-01-04 22:15:02 +01:00
Stranck 866ac5b9d5 Fixed order in sponsor/fursuit count pages 2024-01-04 21:40:06 +01:00
stranck ee25571cd7 Merge pull request 'Fixed sponsor detections' (#4) from drew-dev into stranck-dev
Reviewed-on: #4
2024-01-04 16:42:28 +00:00
Andrea 32e78319fc [Bugfix] missing base profile border style
Added profile pic component in supersponsor page
2024-01-04 14:09:06 +01:00
drew e65a37826c Merge pull request 'stranck-dev' (#3) from stranck-dev into drew-dev
Reviewed-on: #3
2024-01-04 12:59:58 +00:00
Stranck fee70fac7c Updated rainbow animation 2024-01-04 13:38:41 +01:00
stranck 3482c02fc5 Merge pull request 'Merged new propic animation + sponsor fix' (#2) from drew-dev into stranck-dev
Reviewed-on: #2
2024-01-04 11:17:02 +00:00
Stranck 7246973651 Added sponsorcount page 2024-01-04 12:14:43 +01:00
Andrea a70f727009 [Feature] Componentisation of profile pictures
Added a propic component, with the following parameters: order, show flag and show effects

Added a fancy animation on sponsors' profile border
2024-01-03 17:18:31 +01:00
drew 05eb2172ce Merge pull request 'Fixed roomless furs' (#1) from stranck-dev into drew-dev
Reviewed-on: #1
2024-01-03 14:14:12 +00:00
Stranck 3a507b89ac Fixed roomless furs 2023-12-30 12:03:32 +01:00
Stranck 160d186aeb Furizon overlord - working 2023-12-30 11:27:42 +01:00
85 changed files with 6765 additions and 476 deletions

9
.gitignore vendored
View File

@ -154,6 +154,8 @@ cython_debug/
res/propic/propic_*
res/rooms/*
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
@ -161,3 +163,10 @@ res/propic/propic_*
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
config.py
furizon_webinit_riverside2023.tar.gz
diomerdas
furizon.net/site/*
furizon.net.zip
stuff/secrets.py
backups/*
log.txt

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`.

370
admin.py Normal file
View File

@ -0,0 +1,370 @@
from sanic import response, redirect, Blueprint, exceptions
from email_util import send_missing_propic_message
from random import choice
from room import unconfirm_room_by_order
from config import *
from utils import *
from ext import *
from io import StringIO
from sanic.log import logger
import csv
import time
import json
import math
bp = Blueprint("admin", url_prefix="/manage/admin")
@bp.middleware
async def credentials_check(request: Request):
order = await get_order(request)
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if EXTRA_PRINTS:
logger.info(f"Checking admin credentials of {order.code} with secret {order.secret}")
if not order.isAdmin() : raise exceptions.Forbidden("Birichino :)")
@bp.get('/cache/clear')
async def clear_cache(request, order:Order):
success = await request.app.ctx.om.fill_cache()
if not success: raise exceptions.ServerError("An error occurred while loading the cache")
return redirect(f'/manage/admin')
@bp.get('/loginas/<code>')
async def login_as(request, code, order:Order):
dOrder = await get_order_by_code(request, code, throwException=True)
if(dOrder.isAdmin()):
raise exceptions.Forbidden("You can't login as another admin!")
if EXTRA_PRINTS:
logger.info(f"Swapping login: {order.secret} {order.code} -> {dOrder.secret} {code}")
r = redirect(f'/manage/welcome')
r.cookies['foxo_code_ORG'] = order.code
r.cookies['foxo_secret_ORG'] = order.secret
r.cookies['foxo_code'] = code
r.cookies['foxo_secret'] = dOrder.secret
return r
@bp.get('/room/verify')
async def verify_rooms(request, order:Order):
await clear_cache(request, order)
already_checked, success = await request.app.ctx.om.update_cache()
if not already_checked and success:
orders = filter(lambda x: x.status not in ['c', 'e'] and x.room_id == x.code, request.app.ctx.om.cache.values())
await validate_rooms(request, orders, None)
return redirect(f'/manage/admin')
@bp.get('/room/unconfirm/<code>')
async def unconfirm_room(request, code, order:Order):
dOrder = await get_order_by_code(request, code, throwException=True)
await unconfirm_room_by_order(order=dOrder, throw=True, request=request)
return redirect(f'/manage/nosecount')
@bp.get('/room/autoconfirm')
async def autoconfirm_room(request, code, order:Order):
await clear_cache(request, order)
orders = request.app.ctx.om.cache.values()
for order in orders:
if(order.code == order.room_id and not order.room_confirmed and len(order.room_members) == order.room_person_no):
logger.info(f"Auto-Confirming room {order.room_id}")
await confirm_room_by_order(order, request)
return redirect(f'/manage/admin')
@bp.get('/room/delete/<code>')
async def delete_room(request, code, order:Order):
await clear_cache(request, order)
dOrder = await get_order_by_code(request, code, throwException=True)
ppl = await get_people_in_room_by_code(request, code)
for p in ppl:
await p.edit_answer('room_id', None)
await p.edit_answer('room_confirmed', "False")
await p.edit_answer('room_name', None)
await p.edit_answer('pending_room', None)
await p.edit_answer('pending_roommates', None)
await p.edit_answer('room_members', None)
await p.edit_answer('room_owner', None)
await p.edit_answer('room_secret', None)
await p.send_answers()
await dOrder.send_answers()
return redirect(f'/manage/nosecount')
@bp.post('/room/rename/<code>')
async def rename_room(request, code, order:Order):
await clear_cache(request, order)
dOrder = await get_order_by_code(request, code, throwException=True)
name = request.form.get('name')
if len(name) > 64 or len(name) < 4:
raise exceptions.BadRequest("Your room name is invalid. Please try another one.")
await dOrder.edit_answer("room_name", name)
await dOrder.send_answers()
return redirect(f'/manage/nosecount')
@bp.get('/room/wizard')
async def room_wizard(request, order:Order):
'''Tries to autofill unconfirmed rooms and other matches together'''
# Clear cache first
await clear_cache(request, order)
#Separate orders which have incomplete rooms and which have no rooms
all_orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e'] and not value.daily}
orders = {key:value for key,value in sorted(all_orders.items(), key=lambda x: x[1].ans('fursona_name')) if not value.room_confirmed}
# Orders with incomplete rooms
incomplete_orders = {key:value for key,value in orders.items() if value.code == value.room_id and (value.room_person_no - len(value.room_members)) > 0}
# Roomless furs
roomless_orders = {key:value for key,value in orders.items() if not value.room_id and not value.daily}
# Result map
result_map = {}
# Check overflows
room_quota_overflow = {}
for key, value in ITEM_VARIATIONS_MAP['bed_in_room'].items():
room_quota = get_quota(ITEMS_ID_MAP['bed_in_room'], value)
capacity = ROOM_CAPACITY_MAP[key] if key in ROOM_CAPACITY_MAP else 1
current_quota = len(list(filter(lambda y: y.bed_in_room == value and y.room_owner == True, orders.values())))
room_quota_overflow[value] = current_quota - int(room_quota.size / capacity) if room_quota else 0
# Init rooms to remove
result_map["void"] = []
# Remove rooms that are over quota
for room_type, overflow_qty in {key:value for key,value in room_quota_overflow.items() if value > 0}.items():
sorted_rooms = sorted(incomplete_orders.values(), key=lambda r: len(r.room_members))
for room_to_remove in sorted_rooms[:overflow_qty]:
# Room codes to remove
result_map["void"].append(room_to_remove.code)
# Move room members to the roomless list
for member_code in room_to_remove.room_members:
roomless_orders[member_code] = all_orders[member_code]
del incomplete_orders[room_to_remove.code]
# Fill already existing rooms
for room_order in incomplete_orders.items():
room = room_order[1]
to_add = []
missing_slots = room.room_person_no - len(room.room_members)
for i in range(missing_slots):
compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room}
if len(compatible_roomates.items()) == 0: break
# Try picking a roomate that's from the same country and room type
country = room.country.lower()
roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country}
if len(roomless_by_country.items()) > 0:
code_to_add = list(roomless_by_country.keys())[0]
to_add.append(code_to_add)
del roomless_orders[code_to_add]
else:
# If not, add first roomless there is
code_to_add = list(compatible_roomates.keys())[0]
to_add.append(code_to_add)
del roomless_orders[code_to_add]
result_map[room.code] = {
'type': 'add_existing',
'to_add': to_add
}
generated_counter = 0
# Create additional rooms
while len(roomless_orders.items()) > 0:
room = list(roomless_orders.items())[0][1]
to_add = []
missing_slots = room.room_person_no - len(room.room_members)
for i in range(missing_slots):
compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room}
if len(compatible_roomates.items()) == 0: break
# Try picking a roomate that's from the same country and room type
country = room.country.lower()
roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country}
if len(roomless_by_country.items()) > 0:
code_to_add = list(roomless_by_country.keys())[0]
to_add.append(code_to_add)
del roomless_orders[code_to_add]
else:
# If not, add first roomless there is
code_to_add = list(compatible_roomates.keys())[0]
to_add.append(code_to_add)
del roomless_orders[code_to_add]
generated_counter += 1
result_map[room.code] = {
'type': 'new',
'room_name': f'Generated Room {generated_counter}',
'room_type': room.bed_in_room,
'to_add': to_add
}
result_map["infinite"] = { 'to_add': [] }
tpl = request.app.ctx.tpl.get_template('wizard.html')
return html(tpl.render(order=order, all_orders=all_orders, unconfirmed_orders=orders, data=result_map, jsondata=json.dumps(result_map, skipkeys=True, ensure_ascii=False)))
@bp.post('/room/wizard/submit')
async def submit_from_room_wizard(request:Request, order:Order):
'''Will apply changes to the rooms'''
await request.app.ctx.om.fill_cache()
data = json.loads(request.body)
# Phase 1 - Delete all rooms in void
if 'void' in data:
for room_code in data['void']:
ppl = await get_people_in_room_by_code(request, room_code)
for p in ppl:
await p.edit_answer('room_id', None)
await p.edit_answer('room_confirmed', "False")
await p.edit_answer('room_name', None)
await p.edit_answer('pending_room', None)
await p.edit_answer('pending_roommates', None)
await p.edit_answer('room_members', None)
await p.edit_answer('room_owner', None)
await p.edit_answer('room_secret', None)
await p.send_answers()
logger.info(f"Deleted rooms {', '.join(data['void'])}")
# Phase 2 - Join roomless to other rooms or add new rooms
for room_code, value in {key:value for key,value in data.items() if key.lower() not in ['void', 'infinite']}.items():
if not value['to_add'] or len(value['to_add']) == 0: continue
room_order = await request.app.ctx.om.get_order(code=room_code)
# Preconditions
if not room_order: raise exceptions.BadRequest(f"Order {room_code} does not exist.")
if room_order.daily == True: raise exceptions.BadRequest(f"Order {room_code} is daily.")
if room_order.status != 'paid': raise exceptions.BadRequest(f"Order {room_code} hasn't paid.")
if room_order.room_owner:
if room_order.room_person_no < len(room_order.room_members) + (len(value['to_add']) if value['to_add'] else 0):
raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.")
elif room_order.room_person_no < (len(value['to_add']) if value['to_add'] else 0):
raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.")
# Adding roomless orders to existing rooms
if value['type'] == 'add_existing' or value['type'] == 'new':
if value['type'] == 'new':
if room_order.room_owner: exceptions.BadRequest(f"Order {room_code} is already a room owner.")
# Create room data
await room_order.edit_answer('room_name', value['room_name'])
await room_order.edit_answer('room_id', room_order.code)
await room_order.edit_answer('room_secret', ''.join(choice('0123456789') for _ in range(6)))
elif not room_order.room_owner:
raise exceptions.BadRequest(f"Order {room_code} is not a room owner.")
# Add members
for new_member_code in value['to_add']:
pending_member = await request.app.ctx.om.get_order(code=new_member_code)
# Preconditions
if pending_member.daily == True: raise exceptions.BadRequest(f"Order {pending_member.code} is daily.")
if pending_member.status != 'paid': raise exceptions.BadRequest(f"Order {new_member_code} hasn't paid.")
if pending_member.bed_in_room != room_order.bed_in_room: raise exceptions.BadRequest(f"Order {new_member_code} has a different room type than {room_code}.")
if pending_member.room_owner: exceptions.BadRequest(f"Order {new_member_code} is already a room owner.")
if pending_member.room_id and pending_member.room_id not in data['void']: exceptions.BadRequest(f"Order {new_member_code} is in another room.")
await pending_member.edit_answer('room_id', room_order.code)
await pending_member.edit_answer('room_confirmed', "True")
await pending_member.edit_answer('pending_room', None)
await pending_member.send_answers()
logger.info(f"{'Created' if value['type'] == 'new' else 'Edited'} {str(room_order)}")
# Confirm members that were already inside the room
if value['type'] == 'add_existing':
for already_member in list(filter(lambda rm: rm.code in room_order.room_members and rm.code != room_order.code, request.app.ctx.om.cache.values())):
await already_member.edit_answer('room_confirmed', "True")
await already_member.send_answers()
else: raise exceptions.BadRequest(f"Unexpected type ({value['type']})")
await room_order.edit_answer('pending_room', None)
await room_order.edit_answer('pending_roommates', None)
await room_order.edit_answer('room_confirmed', "True")
await room_order.edit_answer('room_members', ','.join(list(set([*room_order.room_members, room_order.code, *value['to_add']]))))
await room_order.send_answers()
await request.app.ctx.om.fill_cache()
return text('done', status=200)
@bp.get('/propic/remind')
async def propic_remind_missing(request, order:Order):
await clear_cache(request, order)
orders = request.app.ctx.om.cache.values()
order: Order
for order in orders:
missingPropic = order.propic is None
missingFursuitPropic = order.is_fursuiter and order.propic_fursuiter is None
if(missingPropic or missingFursuitPropic):
# print(f"{order.code}: prp={missingPropic} fpr={missingFursuitPropic} - {order.name}")
await send_missing_propic_message(order, missingPropic, missingFursuitPropic)
return redirect(f'/manage/admin')
@bp.get('/export/export')
async def export_export(request, order:Order):
await clear_cache(request, order)
data = StringIO()
w = csv.writer(data)
w.writerow(['Status', 'Code', 'First name', 'Last name', 'Nick', 'State', 'Card', 'Artist', 'Fursuiter', 'Sponsorhip', 'Early', 'Late', 'Daily', 'Daily days', 'Shirt', 'Room type', 'Room count', 'Room members', 'Payment', 'Price', 'Refunds', 'Staff'])
orders = request.app.ctx.om.cache.values()
order: Order
for order in orders:
w.writerow([
order.status,
order.code,
order.first_name,
order.last_name,
order.name,
order.country,
order.has_card,
order.is_artist,
order.is_fursuiter,
order.sponsorship,
order.has_early,
order.has_late,
order.daily,
','.join(order.dailyDays),
order.shirt_size,
ROOM_TYPE_NAMES[order.bed_in_room] if order.bed_in_room in ROOM_TYPE_NAMES else "-",
len(order.room_members),
','.join(order.room_members),
order.payment_provider,
order.total - order.fees,
order.refunds,
order.ans('staff_role') or 'attendee',
])
data.seek(0)
str = data.read() #data.read().decode("UTF-8")
data.flush()
data.close()
return raw(str, status=200, headers={'Content-Disposition': f'attachment; filename="export_{int(time.time())}.csv"', "Content-Type": "text/csv; charset=UTF-8"})
@bp.get('/export/hotel')
async def export_hotel(request, order:Order):
await clear_cache(request, order)
data = StringIO()
w = csv.writer(data)
w.writerow(['Room type', 'Room name', 'Room code', 'First name', 'Last name', 'Birthday', 'Address', 'Email', 'Phone number', 'Status'])
orders = sorted(request.app.ctx.om.cache.values(), key=lambda d: (d.room_id if d.room_id != None else "~"))
order: Order
for order in orders:
w.writerow([
ROOM_TYPE_NAMES[order.bed_in_room] if order.bed_in_room in ROOM_TYPE_NAMES else "-",
order.room_name,
order.room_id,
order.first_name,
order.last_name,
order.birth_date,
order.address,
order.email,
order.phone,
order.status,
order.code
])
data.seek(0)
str = data.read() #data.read().decode("UTF-8")
data.flush()
data.close()
return raw(str, status=200, headers={'Content-Disposition': f'attachment; filename="hotel_{int(time.time())}.csv"', "Content-Type": "text/csv; charset=UTF-8"})

19
api.py
View File

@ -9,6 +9,9 @@ import random
import string
import httpx
import json
import traceback
from sanic.log import logger
from email_util import send_app_login_attempt
bp = Blueprint("api", url_prefix="/manage/api")
@ -185,7 +188,8 @@ async def nfc_scan(request, nfc_id):
@bp.get("/get_token/<code>/<login_code>")
async def get_token_from_code(request, code, login_code):
if not code in request.app.ctx.login_codes:
print(request.app.ctx.login_codes)
if DEV_MODE and EXTRA_PRINTS:
logger.debug(request.app.ctx.login_codes)
return response.json({'ok': False, 'error': 'You need to reauthenticate. The code has expired.'}, status=401)
if request.app.ctx.login_codes[code][1] == 0:
@ -219,16 +223,9 @@ async def get_token(request, code):
request.app.ctx.login_codes[code] = [''.join(random.choice(string.digits) for _ in range(6)), 3]
try:
msg = MIMEText(f"Hello {user.name}!\n\nWe have received a request to login in the app. If you didn't do this, please ignore this email. Somebody is probably playing with you.\n\nYour login code is: {request.app.ctx.login_codes[code][0]}\n\nPlease do not tell this to anybody!")
msg['Subject'] = '[Furizon] Your login code'
msg['From'] = 'Furizon <no-reply@furizon.net>'
msg['To'] = f"{user.name} <{user.email}>"
s = smtplib.SMTP_SSL(SMTP_HOST)
s.login(SMTP_USER, SMTP_PASSWORD)
s.sendmail(msg['From'], msg['to'], msg.as_string())
s.quit()
except:
await send_app_login_attempt(user, request.app.ctx.login_codes[code][0])
except Exception:
logger.error(f"[API] [GET_TOKEN] Error while sending email.\n{traceback.format_exc()}")
return response.json({'ok': False, 'error': 'There has been an issue sending your e-mail. Please try again later or report to an admin.'}, status=500)
return response.json({'ok': True, 'message': 'A login code has been sent to your email.'})

172
app.py
View File

@ -1,20 +1,24 @@
from sanic import Sanic, response, exceptions
from sanic.response import text, html, redirect, raw
from jinja2 import Environment, FileSystemLoader
from time import time
from time import time, sleep
import httpx
import re
import json
import logging
from os.path import join
from ext import *
from config import *
from aztec_code_generator import AztecCode
from propic import resetDefaultPropic
from io import BytesIO
from asyncio import Queue
from messages import LOCALES
import sqlite3
log = logging.getLogger()
import requests
import sys
from sanic.log import logger, logging, access_logger
from metrics import *
from email_util import killSmptClient
import pretixClient
import traceback
app = Sanic(__name__)
app.static("/res", "res/")
@ -25,33 +29,49 @@ app.ext.add_dependency(Quotas, get_quotas)
from room import bp as room_bp
from propic import bp as propic_bp
from karaoke import bp as karaoke_bp
from export import bp as export_bp
from stats import bp as stats_bp
from api import bp as api_bp
from carpooling import bp as carpooling_bp
from checkin import bp as checkin_bp
from admin import bp as admin_bp
app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp])
app.blueprint([room_bp, karaoke_bp, propic_bp, stats_bp, api_bp, carpooling_bp, checkin_bp, admin_bp])
@app.exception(exceptions.SanicException)
async def clear_session(request, exception):
tpl = app.ctx.tpl.get_template('error.html')
r = html(tpl.render(exception=exception))
if exception.status_code == 403:
del r.cookies["foxo_code"]
del r.cookies["foxo_secret"]
async def clear_session(response):
response.delete_cookie("foxo_code")
response.delete_cookie("foxo_secret")
@app.exception(exceptions.SanicException if DEV_MODE else Exception)
async def handleException(request, exception):
incErrorNo()
logger.warning(f"{request} -> {exception}")
statusCode = exception.status_code if hasattr(exception, 'status_code') else 500
try:
tpl = app.ctx.tpl.get_template('error.html')
r = html(tpl.render(exception=exception, status_code=statusCode), status=statusCode)
except:
traceback.print_exc()
if statusCode == 403:
await clear_session(r)
return r
@app.before_server_start
async def main_start(*_):
print(">>>>>> main_start <<<<<<")
logger.info(f"[{app.name}] >>>>>> main_start <<<<<<")
logger.setLevel(LOG_LEVEL)
access_logger.addFilter(MetricsFilter())
app.config.REQUEST_MAX_SIZE = PROPIC_MAX_FILE_SIZE * 3
app.ctx.om = OrderManager()
if FILL_CACHE:
log.info("Filling cache!")
await app.ctx.om.fill_cache()
log.info("Cache fill done!")
checked, success = await app.ctx.om.update_cache(check_itemsQuestions=True)
if checked and not success:
logger.error(f"[{app.name}] Failure in app startup: An error occurred while loading items or questions or cache.")
app.stop()
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
@ -60,7 +80,13 @@ async def main_start(*_):
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
app.ctx.tpl.globals.update(time=time)
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
app.ctx.tpl.globals.update(ITEM_IDS=ITEM_IDS)
app.ctx.tpl.globals.update(LOCALES=LOCALES)
app.ctx.tpl.globals.update(ITEMS_ID_MAP=ITEMS_ID_MAP)
app.ctx.tpl.globals.update(ITEM_VARIATIONS_MAP=ITEM_VARIATIONS_MAP)
app.ctx.tpl.globals.update(ROOM_TYPE_NAMES=ROOM_TYPE_NAMES)
app.ctx.tpl.globals.update(PROPIC_MIN_SIZE=PROPIC_MIN_SIZE)
app.ctx.tpl.globals.update(PROPIC_MAX_SIZE=PROPIC_MAX_SIZE)
app.ctx.tpl.globals.update(PROPIC_MAX_FILE_SIZE=sizeof_fmt(PROPIC_MAX_FILE_SIZE))
app.ctx.tpl.globals.update(int=int)
app.ctx.tpl.globals.update(len=len)
@ -72,6 +98,11 @@ async def gen_barcode(request, code):
return raw(img.getvalue(), content_type="image/png")
@app.route("/manage/lol")
async def lol(request: Request):
await get_quotas(request)
return text('hi')
@app.route(f"/{ORGANIZER}/{EVENT_NAME}/order/<code>/<secret>/open/<secret2>")
async def redirect_explore(request, code, secret, order: Order, secret2=None):
@ -79,17 +110,16 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None):
if order and order.code != code: order = None
if not order:
async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, f"orders/{code}/"), headers=headers)
print(res.json())
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.")
res = res.json()
if secret != res['secret']:
raise exceptions.Forbidden("The secret part of the url is not correct. Check your E-Mail for the correct link, or contact support!")
r.cookies['foxo_code'] = code
r.cookies['foxo_secret'] = secret
res = await pretixClient.get(f"orders/{code}/", expectedStatusCodes=None)
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.")
res = res.json()
if secret != res['secret']:
raise exceptions.Forbidden("The secret part of the url is not correct. Check your E-Mail for the correct link, or contact support!")
r.cookies['foxo_code'] = code
r.cookies['foxo_secret'] = secret
return r
@app.route("/manage/privacy")
@ -102,6 +132,11 @@ async def welcome(request, order: Order, quota: Quotas):
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if order.ans("propic_file") is None:
await resetDefaultPropic(request, order, False)
if order.ans("propic_fursuiter_file") is None:
await resetDefaultPropic(request, order, True)
pending_roommates = []
if order.pending_roommates:
@ -123,10 +158,10 @@ async def welcome(request, order: Order, quota: Quotas):
if member_id == order.code:
room_members.append(order)
else:
room_members.append(await app.ctx.om.get_order(code=member_id, cached=True))
room_members.append(await app.ctx.om.get_order(code=member_id, cached=True))
tpl = app.ctx.tpl.get_template('welcome.html')
return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates))
return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates, ROOM_ERROR_MESSAGES=ROOM_ERROR_TYPES))
@app.route("/manage/download_ticket")
@ -138,19 +173,76 @@ async def download_ticket(request, order: Order):
if not order.status != 'confirmed':
raise exceptions.Forbidden("You are not allowed to download this ticket.")
async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, f"orders/{order.code}/download/pdf/"), headers=headers)
res = await pretixClient.get(f"orders/{order.code}/download/pdf/", expectedStatusCodes=[200, 404, 409, 403])
if res.status_code == 409:
if res.status_code == 409 or res.status_code == 404:
raise exceptions.SanicException("Your ticket is still being generated. Please try again later!", status_code=res.status_code)
elif res.status_code == 403:
raise exceptions.SanicException("You can download your ticket only after the order has been confirmed and paid. Try later!", status_code=400)
return raw(res.content, content_type='application/pdf')
@app.route("/manage/admin")
async def admin(request, order: Order):
await request.app.ctx.om.update_cache()
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if EXTRA_PRINTS:
logger.info(f"Checking admin credentials of {order.code} with secret {order.secret}")
if not order.isAdmin(): raise exceptions.Forbidden("Birichino :)")
tpl = app.ctx.tpl.get_template('admin.html')
return html(tpl.render(order=order))
@app.route("/manage/logout")
async def logour(request):
raise exceptions.Forbidden("You have been logged out.", status_code=403)
async def logout(request):
orgCode = request.cookies.get("foxo_code_ORG")
orgSecret = request.cookies.get("foxo_secret_ORG")
if orgCode != None and orgSecret != None:
r = redirect(f'/manage/welcome')
r.cookies['foxo_code'] = orgCode
r.cookies['foxo_secret'] = orgSecret
r.delete_cookie("foxo_code_ORG")
r.delete_cookie("foxo_secret_ORG")
return r
raise exceptions.Forbidden("You have been logged out.")
@app.signal("server.shutdown.before")
async def sigintHandler(app, loop):
killSmptClient()
@app.get(METRICS_PATH)
async def metrics(request):
return text(getMetricsText() + "\n" + getRoomCountersText(request))
@app.on_request
async def countReqs(request : Request):
global METRICS_REQ_NO
if(request.path != METRICS_PATH):
incReqNo()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8188, dev=DEV_MODE)
# Wait for pretix in server reboot
# Using a docker configuration, pretix may be unable to talk with postgres if postgres' service started before it.
# To fix this issue I added a After=pretix.service to the [Unit] section of /lib/systemd/system/postgresql@.service
# to let it start in the correct order. The following piece of code makes sure that pretix is running and can talk to
# postgres before actually starting the reserved area, since this operation requires a cache-fill in startup
print("Waiting for pretix to be up and running", file=sys.stderr)
while not SKIP_HEALTHCHECK:
print("Trying connecting to pretix...", file=sys.stderr)
try:
incPretixRead()
res = requests.get(base_url_event, headers=headers)
res = res.json()
if(res['slug'] == EVENT_NAME):
print("Healtchecking...", file=sys.stderr)
incPretixRead()
res = requests.get(join(domain, "healthcheck"), headers=headers)
if(res.status_code == 200):
break
except:
pass
sleep(5)
print("Connected to pretix!", file=sys.stderr)
app.run(host="127.0.0.1", port=8188, dev=DEV_MODE, access_log=ACCESS_LOG)

View File

@ -10,7 +10,6 @@ async def carpooling_list(request, order: Order, error=None):
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
orders = [value for value in request.app.ctx.om.cache.values() if value.status not in ['c', 'e'] and value.carpooling_message]
print(orders)
tpl = request.app.ctx.tpl.get_template('carpooling.html')

View File

@ -2,7 +2,7 @@ from sanic.response import html, redirect, text
from sanic import Blueprint, exceptions, response
from random import choice
from ext import *
from config import headers, base_url
from config import headers, base_url_event
from PIL import Image
from os.path import isfile
from os import unlink
@ -11,6 +11,8 @@ from hashlib import sha224
from time import time
from urllib.parse import unquote
import json
from metrics import *
import pretixClient
bp = Blueprint("checkin", url_prefix="/checkin")
@ -63,8 +65,7 @@ async def do_checkin(request):
await order.send_answers()
if not order.checked_in:
async with httpx.AsyncClient() as client:
res = await client.post(base_url.replace('events/beyond/', 'checkinrpc/redeem/'), json={'secret': order.barcode, 'source_type': 'barcode', 'type': 'entry', 'lists': [3,]}, headers=headers)
await pretixClient.post("", baseUrl=base_url_event.replace(f'events/{EVENT_NAME}/', 'checkinrpc/redeem/'), json={'secret': order.barcode, 'source_type': 'barcode', 'type': 'entry', 'lists': [3,]})
tpl = request.app.ctx.tpl.get_template('checkin_3.html')
return html(tpl.render(order=order, room_owner=room_owner, roommates=roommates))

View File

@ -1,41 +1,154 @@
from sanic.log import logging
LOG_LEVEL = logging.DEBUG
API_TOKEN = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
ORGANIZER = 'furizon'
EVENT_NAME = 'river-side-2023'
API_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxx'
HOSTNAME = 'your-pretix-hostname'
EVENT_NAME = 'overlord'
HOSTNAME = 'reg.furizon.net'
SKIP_HEALTHCHECK = False
headers = {'Host': HOSTNAME, 'Authorization': f'Token {API_TOKEN}'}
base_url = f"https://{HOSTNAME}/api/v1/organizers/{ORGANIZER}/events/{EVENT_NAME}/"
domain = "http://urlllllllllllllllllllll/"
base_url = "{domain}api/v1/"
base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/"
PROPIC_DEADLINE = 1683575684
FILL_CACHE = True
DEV_MODE = True
ITEM_IDS = {
'ticket': [90,],
'membership_card': [91,],
'sponsorship': [], # first one = normal, second = super
'early_arrival': [],
'late_departure': [],
'room': 98
}
# Create a bunch of "room" items which will get added to the order once somebody gets a room.
ROOM_MAP = {
1: 16,
2: 17,
3: 18,
4: 19,
5: 20
}
PROPIC_DEADLINE = 9999999999
PROPIC_MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
PROPIC_MAX_SIZE = (2048, 2048) # (Width, Height)
PROPIC_MIN_SIZE = (125, 125) # (Width, Height)
# This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id.
TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
TG_CHAT_ID = -1234567
import httpx
# Number of tries for a request to the pretix's backend
PRETIX_REQUESTS_MAX = 3
PRETIX_REQUESTS_TIMEOUT = httpx.Timeout(15.0, read=30.0, connect=45.0, pool=None) # Timeout for httpx requests in seconds
# These order codes have additional functions.
ADMINS = ['XXXXX', 'YYYYY']
# A list of staff_roles
ADMINS_PRETIX_ROLE_NAMES = ["Reserved Area admin", "main staff"]
SMTP_HOST = 'your-smtp-host.com'
SMTP_USER = 'username'
SMTP_PASSWORD = 'password'
SMTP_HOST = 'host'
SMTP_PORT = 0
SMTP_USER = 'user'
SMTP_PASSWORD = 'pw'
EMAIL_SENDER_NAME = "Fantastic Furcon Wow"
EMAIL_SENDER_MAIL = "no-reply@thisIsAFantasticFurconWowItIsWonderful.cuteFurries.ovh"
SMPT_CLIENT_CLOSE_TIMEOUT = 60 * 15 # 15 minutes
FILL_CACHE = True
CACHE_EXPIRE_TIME = 60 * 60 * 4
DEV_MODE = True
ACCESS_LOG = True
EXTRA_PRINTS = True
UNCONFIRM_ROOMS_ENABLE = True
METRICS_PATH = "/welcome/metrics"
# Additional configured locales.
# If an order has a country that's not listed here,
# Will default to an english preference.
AVAILABLE_LOCALES = ['it',]
# Metadata property for item-id mapping
METADATA_NAME = "item_name"
# Metadata property for internal category mapping (not related to pretix's category)
METADATA_CATEGORY = "category_name"
SPONSORSHIP_COLOR_MAP = {
'super': (251, 140, 0),
'normal': (142, 36, 170)
}
# Quotes
QUOTES_LIST = []
# Maps Products metadata name <--> ID
ITEMS_ID_MAP = {
'early_bird_ticket': None,
'regular_ticket': None,
'staff_ticket': None,
'daily_ticket': None,
'regular_bundle_sponsor_ticket': None,
'sponsorship_item': None,
'early_arrival_admission': None,
'late_departure_admission': None,
'membership_card_item': None,
'bed_in_room': None,
'room_type': None,
'room_guest': None,
'daily_1': None,
'daily_2': None,
'daily_3': None,
'daily_4': None,
'daily_5': None
}
# Maps Products' variants metadata name <--> ID
ITEM_VARIATIONS_MAP = {
'sponsorship_item': {
'sponsorship_item_normal': None,
'sponsorship_item_super': None
},
'bed_in_room': {
'bed_in_room_no_room': None,
'bed_in_room_main_1': None,
'bed_in_room_main_2': None,
'bed_in_room_main_3': None,
'bed_in_room_main_4': None,
'bed_in_room_main_5': None,
'bed_in_room_overflow1_2': None,
},
'room_type': {
'single': None,
'double': None,
'triple': None,
'quadruple': None,
'quintuple': None
},
'room_guest': {
'single': None,
'double': None,
'triple': None,
'quadruple': None,
'quintuple': None
}
}
ADMINS_PRETIX_ROLE_NAMES = ["Reserved Area admin", "main staff"]
# Links Products' variants' ids with the internal category name
CATEGORIES_LIST_MAP = {
'tickets': [],
'memberships': [],
'sponsorships': [],
'tshirts': [],
'extra_days': [],
'rooms': [],
'dailys': []
}
# Create a bunch of "room" items which will get added to the order once somebody gets a room.
# Map item_name -> room capacity
ROOM_CAPACITY_MAP = {
# Default
'bed_in_room_no_room': 0,
# SACRO CUORE
'bed_in_room_main_1': 1,
'bed_in_room_main_2': 2,
'bed_in_room_main_3': 3,
'bed_in_room_main_4': 4,
'bed_in_room_main_5': 5,
# OVERFLOW 1
'bed_in_room_overflow1_2': 2,
}
# Autofilled. Maps roomTypeId -> roomName
ROOM_TYPE_NAMES = { }

21
connector.py Normal file
View File

@ -0,0 +1,21 @@
# To connect with pretix's data
# WIP to not commit
# DrewTW
import string
import httpx
import json
from ext import *
from config import *
from sanic import response
from sanic import Blueprint
from sanic.log import logger
def checkConfig():
if (not DEV_MODE) and DUMMY_DATA:
logger.warn('It is strongly unadvised to use dummy data in production')
def getOrders(page):
return None
def getOrder(code):
return None

Binary file not shown.

Binary file not shown.

3048
dummy.json Normal file

File diff suppressed because it is too large Load Diff

124
email_util.py Normal file
View File

@ -0,0 +1,124 @@
from sanic import Sanic
from sanic.log import logger
import ssl
from ssl import SSLContext
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from messages import ROOM_ERROR_TYPES
import smtplib
from messages import *
from config import *
from jinja2 import Environment, FileSystemLoader
from threading import Timer, Lock
def killSmptClient():
global sslLock
global sslTimer
global smptSender
sslTimer.cancel()
sslLock.acquire()
if(smptSender is not None):
logger.debug('[SMPT] Closing smpt client')
smptSender.quit() # it calls close() inside
smptSender = None
sslLock.release()
async def openSmptClient():
global sslLock
global sslTimer
global sslContext
global smptSender
sslTimer.cancel()
sslLock.acquire()
if(smptSender is None):
logger.debug('[SMPT] Opening smpt client')
client : smtplib.SMTP = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
client.starttls(context=sslContext)
client.login(SMTP_USER, SMTP_PASSWORD)
smptSender = client
sslLock.release()
sslTimer = createTimer()
sslTimer.start()
def createTimer():
return Timer(SMPT_CLIENT_CLOSE_TIMEOUT, killSmptClient)
sslLock : Lock = Lock()
sslTimer : Timer = createTimer()
sslContext : SSLContext = ssl.create_default_context()
smptSender : smtplib.SMTP = None
async def sendEmail(message : MIMEMultipart):
message['From'] = f'{EMAIL_SENDER_NAME} <{EMAIL_SENDER_MAIL}>'
await openSmptClient()
logger.debug(f"[SMPT] Sending mail {message['From']} -> {message['to']} '{message['Subject']}'")
sslLock.acquire()
smptSender.sendmail(message['From'], message['to'], message.as_string())
sslLock.release()
def render_email_template(title = "", body = ""):
tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html')
return str(tpl.render(title=title, body=body))
async def send_unconfirm_message(room_order, orders):
memberMessages = []
issues_plain = ""
issues_html = "<ul>"
for err in room_order.room_errors:
errId = err[1]
order = err[0]
orderStr = ""
if order is not None:
orderStr = f"{order}: "
if errId in ROOM_ERROR_TYPES.keys():
issues_plain += f"{orderStr}{ROOM_ERROR_TYPES[errId]}\n"
issues_html += f"<li>{orderStr}{ROOM_ERROR_TYPES[errId]}</li>"
issues_html += "</ul>"
for member in orders:
if(member.status != 'canceled'):
plain_body = EMAILS_TEXT["ROOM_UNCONFIRM_TEXT"]['plain'].format(member.name, room_order.room_name, issues_plain)
html_body = render_email_template(EMAILS_TEXT["ROOM_UNCONFIRM_TITLE"], EMAILS_TEXT["ROOM_UNCONFIRM_TEXT"]['html'].format(member.name, room_order.room_name, issues_html))
plain_text = MIMEText(plain_body, "plain")
html_text = MIMEText(html_body, "html")
message = MIMEMultipart("alternative")
message.attach(plain_text)
message.attach(html_text)
message['Subject'] = f'[{EMAIL_SENDER_NAME}] Your room cannot be confirmed'
message['To'] = f"{member.name} <{member.email}>"
memberMessages.append(message)
if len(memberMessages) == 0: return
for message in memberMessages:
await sendEmail(message)
async def send_missing_propic_message(order, missingPropic, missingFursuitPropic):
t = []
if(missingPropic): t.append("your propic")
if(missingFursuitPropic): t.append("your fursuit's badge")
missingText = " and ".join(t)
plain_body = EMAILS_TEXT["MISSING_PROPIC_TEXT"]['plain'].format(order.name, missingText)
html_body = render_email_template(EMAILS_TEXT["MISSING_PROPIC_TITLE"], EMAILS_TEXT["MISSING_PROPIC_TEXT"]['html'].format(order.name, missingText))
plain_text = MIMEText(plain_body, "plain")
html_text = MIMEText(html_body, "html")
message = MIMEMultipart("alternative")
message.attach(plain_text)
message.attach(html_text)
message['Subject'] = f"[{EMAIL_SENDER_NAME}] You haven't uploaded your badges yet!"
message['To'] = f"{order.name} <{order.email}>"
await sendEmail(message)
async def send_app_login_attempt(user, loginCode):
#TODO: Format a proper email and add it to messages.py
msg = MIMEText(f"Hello {user.name}!\n\nWe have received a request to login in the app. If you didn't do this, please ignore this email. Somebody is probably playing with you.\n\nYour login code is: {loginCode}\n\nPlease do not tell this to anybody!")
msg['Subject'] = '[Furizon] Your login code'
msg['To'] = f"{user.name} <{user.email}>"
await sendEmail(msg)

View File

@ -1,87 +0,0 @@
from sanic.response import text
from sanic import Blueprint, exceptions
from ext import *
from config import headers, ADMINS, ORGANIZER, EVENT_NAME
bp = Blueprint("export", url_prefix="/manage/export")
@bp.route("/export.csv")
async def export_csv(request, order: Order):
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if order.code not in ADMINS: raise exceptions.Forbidden("Birichino :)")
page = 0
orders = {}
ret = 'status;code;nome;cognome;nick;nazione;tessera;artista;fursuiter;sponsorship;early;late;shirt;roomsize;roommembers;payment;price;refunds;staff\n'
while 1:
page += 1
r = httpx.get(f'https://reg.furizon.net/api/v1/organizers/{ORGANIZER}/events/{EVENT_NAME}/orders/?page={page}', headers=headers)
if r.status_code == 404: break
for r in r.json()['results']:
o = Order(r)
orders[o.code] = o
ret += (';'.join(map(lambda x: str(x),
[
o.status,
o.code,
o.first_name,
o.last_name,
o.name,
o.country,
o.has_card or '',
o.is_artist or '',
o.is_fursuiter or '',
o.sponsorship or '',
o.has_early or '',
o.has_late or '',
o.shirt_size,
len(o.room_members),
','.join(o.room_members),
o.payment_provider,
o.total-o.fees,
o.refunds,
o.ans('staff_role') or 'attendee',
]))) + "\n"
return text(ret)
@bp.route("/hotel_export.csv")
async def export_hotel_csv(request, order: Order):
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if order.code not in ['HWUC9','9YKGJ']: raise exceptions.Forbidden("Birichino :)")
page = 0
orders = {}
ret = 'code;nome;cognome;datanascita;posnascita;indirizzo;mail;status\n'
while 1:
page += 1
r = httpx.get(f'https://reg.furizon.net/api/v1/organizers/{ORGANIZER}/events/{EVENT_NAME}/orders/?page={page}', headers=headers)
if r.status_code == 404: break
for r in r.json()['results']:
o = Order(r)
orders[o.code] = o
ret += (';'.join(map(lambda x: str(x),
[
o.code,
o.first_name,
o.last_name,
o.birth_date,
o.birth_location,
o.address,
o.email,
o.status
]))) + "\n"
return text(ret)

354
ext.py
View File

@ -1,11 +1,18 @@
from dataclasses import dataclass
from sanic import Request, exceptions
from sanic import Request, exceptions, Sanic
import httpx
import re
from utils import *
from config import *
from os.path import join
import json
from sanic.log import logger
from time import time
from metrics import *
import asyncio
from threading import Lock
import pretixClient
import traceback
@dataclass
class Order:
@ -13,8 +20,13 @@ class Order:
self.time = time()
self.data = data
if(len(self.data['positions']) == 0):
for fee in data['fees']:
if(fee['fee_type'] == "cancellation"):
self.data['status'] = 'c'
self.status = {'n': 'pending', 'p': 'paid', 'e': 'expired', 'c': 'canceled'}[self.data['status']]
self.secret = data['secret']
if not len(self.data['positions']):
self.status = 'canceled'
@ -27,41 +39,65 @@ class Order:
self.sponsorship = None
self.has_early = False
self.has_late = False
self.first_name = None
self.last_name = None
self.first_name = "None"
self.last_name = "None"
self.country = 'xx'
self.address = None
self.checked_in = False
self.room_type = None
self.daily = False
self.dailyDays = []
self.bed_in_room = -1
self.room_person_no = -1
self.answers = []
self.position_id = -1
self.position_positionid = -1
self.position_positiontypeid = -1
self.barcode = "None"
idata = data['invoice_address']
if idata:
self.address = f"{idata['street']} - {idata['zipcode']} {idata['city']} - {idata['country']}"
self.address = f"{idata['street'].strip()} - {idata['zipcode'].strip()} {idata['city'].strip()} - {idata['country'].strip()}".replace("\n", "").replace("\r", "")
self.country = idata['country']
for p in self.data['positions']:
if p['item'] in (ITEM_IDS['ticket'] + ITEM_IDS['daily']):
if p['item'] in CATEGORIES_LIST_MAP['tickets']:
self.position_id = p['id']
self.position_positionid = p['positionid']
self.position_positiontypeid = p['item']
self.answers = p['answers']
for i, ans in enumerate(self.answers):
if(TYPE_OF_QUESTIONS[self.answers[i]['question']] == QUESTION_TYPES['file_upload']):
self.answers[i]['answer'] = "file:keep"
self.barcode = p['secret']
self.checked_in = bool(p['checkins'])
if p['item'] in CATEGORIES_LIST_MAP['dailys']:
self.daily = True
self.dailyDays.append(CATEGORIES_LIST_MAP['dailys'].index(p['item']))
if p['item'] in ITEM_IDS['membership_card']:
if p['item'] in CATEGORIES_LIST_MAP['memberships']:
self.has_card = True
if p['item'] in ITEM_IDS['sponsorship']:
self.sponsorship = 'normal' if p['variation'] == ITEMS_IDS['sponsorship'][0] else 'super'
if p['item'] == ITEMS_ID_MAP['sponsorship_item']:
sponsorshipType = key_from_value(ITEM_VARIATIONS_MAP['sponsorship_item'], p['variation'])
self.sponsorship = sponsorshipType[0].replace ('sponsorship_item_', '') if len(sponsorshipType) > 0 else None
if p['attendee_name']:
self.first_name = p['attendee_name_parts']['given_name']
self.last_name = p['attendee_name_parts']['family_name']
if p['item'] == ITEM_IDS['early_arrival']:
if p['item'] == ITEMS_ID_MAP['early_arrival_admission']:
self.has_early = True
if p['item'] == ITEM_IDS['late_departure']:
if p['item'] == ITEMS_ID_MAP['late_departure_admission']:
self.has_late = True
if p['item'] == ITEMS_ID_MAP['bed_in_room']:
roomTypeLst = key_from_value(ITEM_VARIATIONS_MAP['bed_in_room'], p['variation'])
roomTypeId = roomTypeLst[0] if len(roomTypeLst) > 0 else None
self.bed_in_room = p['variation']
self.room_person_no = ROOM_CAPACITY_MAP[roomTypeId] if roomTypeId in ROOM_CAPACITY_MAP else self.room_person_no
self.total = float(data['total'])
self.fees = 0
@ -77,6 +113,12 @@ class Order:
self.payment_provider = data['payment_provider']
self.comment = data['comment']
self.phone = data['phone']
self.room_errors = []
self.loadAns()
if(self.bed_in_room < 0 and not self.daily):
self.status = "canceled" # Must refer to the previous status assignment
def loadAns(self):
self.shirt_size = self.ans('shirt_size')
self.is_artist = True if self.ans('is_artist') != 'No' else False
self.is_fursuiter = True if self.ans('is_fursuiter') != 'No' else False
@ -97,16 +139,20 @@ class Order:
self.pending_room = self.ans('pending_room')
self.pending_roommates = self.ans('pending_roommates').split(',') if self.ans('pending_roommates') else []
self.room_members = self.ans('room_members').split(',') if self.ans('room_members') else []
self.room_owner = (self.code == self.room_id)
self.room_owner = (self.code is not None and self.room_id is not None and self.code.strip() == self.room_id.strip())
self.room_secret = self.ans('room_secret')
self.app_token = self.ans('app_token')
self.nfc_id = self.ans('nfc_id')
self.can_scan_nfc = True if self.ans('can_scan_nfc') != 'No' else False
self.actual_room = self.ans('actual_room')
self.staff_role = self.ans('staff_role')
self.telegram_username = self.ans('telegram_username').strip('@') if self.ans('telegram_username') else None
self.shuttle_bus = self.ans('shuttle_bus')
def __getitem__(self, var):
return self.data[var]
def set_room_errors (self, to_set):
self.room_errors = to_set
def ans(self, name):
for p in self.data['positions']:
@ -116,59 +162,129 @@ class Order:
return bool(a['answer'] == 'True')
return a['answer']
return None
def isBadgeValid (self):
return self.ans('propic') and (not self.is_fursuiter or self.ans('propic_fursuiter'))
def isAdmin (self):
return self.code in ADMINS or self.staff_role in ADMINS_PRETIX_ROLE_NAMES
async def edit_answer_fileUpload(self, name, fileName, mimeType, data : bytes):
if(mimeType != None and data != None):
localHeaders = dict(headers)
localHeaders['Content-Type'] = mimeType
localHeaders['Content-Disposition'] = f'attachment; filename="{fileName}"'
res = await pretixClient.post("upload", baseUrl=base_url, headers=localHeaders, content=data, expectedStatusCodes=[201])
res = res.json()
await self.edit_answer(name, res['id'])
else:
await self.edit_answer(name, None)
self.loadAns()
async def edit_answer(self, name, new_answer):
found = False
self.pending_update = True
for key in range(len(self.answers)):
if self.answers[key].get('question_identifier', None) == name:
if new_answer != None:
print('EXISTING ANSWER UPDATE', name, '=>', new_answer)
if DEV_MODE and EXTRA_PRINTS: logger.debug('[ANSWER EDIT] EXISTING ANSWER UPDATE %s => %s', name, new_answer)
self.answers[key]['answer'] = new_answer
found = True
else:
print('DEL ANSWER', name, '=>', new_answer)
if DEV_MODE and EXTRA_PRINTS: logger.debug('[ANSWER EDIT] DEL ANSWER %s => %s', name, new_answer)
del self.answers[key]
break
if (not found) and (new_answer is not None):
async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, 'questions/'), headers=headers)
res = res.json()
res = await pretixClient.get("questions/")
res = res.json()
for r in res['results']:
if r['identifier'] != name: continue
print('ANSWER UPDATE', name, '=>', new_answer)
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'[ANSWER EDIT] %s => %s', name, new_answer)
self.answers.append({
'question': r['id'],
'answer': new_answer,
'options': r['options']
})
self.loadAns()
async def send_answers(self):
async with httpx.AsyncClient() as client:
print("POSITION ID IS", self.position_id)
for i, ans in enumerate(self.answers):
# Fix for karaoke fields
if ans['question'] == 40:
del self.answers[i]['options']
del self.answers[i]['option_identifiers']
res = await client.patch(join(base_url, f'orderpositions/{self.position_id}/'), headers=headers, json={'answers': self.answers})
if res.status_code != 200:
if DEV_MODE and EXTRA_PRINTS: logger.debug("[ANSWER POST] POSITION ID IS %s", self.position_id)
for i, ans in enumerate(self.answers):
if TYPE_OF_QUESTIONS[ans['question']] == QUESTION_TYPES["multiple_choice_from_list"]: # if multiple choice
identifier = ans['question_identifier']
if self.ans(identifier) == "": #if empty answer
await self.edit_answer(identifier, None)
# Fix for karaoke fields
#if ans['question'] == 40:
# del self.answers[i]['options']
# del self.answers[i]['option_identifiers']
ans = [] if self.status == "canceled" else self.answers
res = await pretixClient.patch(f'orderpositions/{self.position_id}/', json={'answers': ans}, expectedStatusCodes=None)
if res.status_code != 200:
e = res.json()
if "answers" in e:
for ans, err in zip(self.answers, res.json()['answers']):
if err:
print('ERROR ON', ans, err)
logger.error ('[ANSWERS SENDING] ERROR ON %s %s', ans, err)
else:
logger.error("[ANSWERS SENDING] GENERIC ERROR. Response: '%s'", str(e))
raise exceptions.ServerError('There has been an error while updating this answers.')
raise exceptions.ServerError('There has been an error while updating this answers.')
for i, ans in enumerate(self.answers):
if(TYPE_OF_QUESTIONS[self.answers[i]['question']] == QUESTION_TYPES['file_upload']):
self.answers[i]['answer'] = "file:keep"
self.pending_update = False
self.time = -1
self.loadAns()
def get_language(self):
return self.country.lower() if self.country.lower() in AVAILABLE_LOCALES else 'en'
def __str__(self):
to_return = f"{'Room' if self.room_owner else 'Order'} {self.code}"
if self.room_owner == True:
to_return = f"{to_return} [ members = {self.room_members} ]"
return to_return
def __repr__(self):
to_return = f"{'Room' if self.room_owner == True else 'Order'} {self.code}"
if self.room_owner == True:
to_return = f"{to_return} [ members = {self.room_members} ]"
return to_return
@dataclass
class Quota:
def __init__(self, data):
self.items = data['items'] if 'items' in data else []
self.variations = data['variations'] if 'variations' in data else []
self.available = data['available'] if 'available' in data else False
self.size = data['size'] if 'size' in data else 0
self.available_number = data['available_number'] if 'available_number' in data else 0
def has_item (self, id: int=-1, variation: int=None):
return id in self.items if not variation else (id in self.items and variation in self.variations)
def get_left (self):
return self.available_number
def __repr__(self):
return f'Quota [items={self.items}, variations={self.variations}] [{self.available_number}/{self.size}]'
def __str__(self):
return f'Quota [items={self.items}, variations={self.variations}] [{self.available_number}/{self.size}]'
def get_quota(item: int, variation: int = None) -> Quota:
for q in QUOTA_LIST:
if (q.has_item(item, variation)): return q
return None
@dataclass
class Quotas:
@ -182,11 +298,25 @@ class Quotas:
return 0
async def get_quotas(request: Request=None):
async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, 'quotas/?order=id&with_availability=true'), headers=headers)
res = await pretixClient.get('quotas/?order=id&with_availability=true')
res = res.json()
return Quotas(res)
async def load_item_quotas() -> bool:
global QUOTA_LIST
QUOTA_LIST = []
logger.info ('[QUOTAS] Loading quotas...')
success = True
try:
res = await pretixClient.get('quotas/?order=id&with_availability=true')
res = res.json()
return Quotas(res)
for quota_data in res['results']:
QUOTA_LIST.append (Quota(quota_data))
except Exception:
logger.warning(f"[QUOTAS] Error while loading quotas.\n{traceback.format_exc()}")
success = False
return success
async def get_order(request: Request=None):
await request.receive_body()
@ -194,37 +324,105 @@ async def get_order(request: Request=None):
class OrderManager:
def __init__(self):
self.lastCacheUpdate = 0
self.updating : Lock = Lock()
self.empty()
def empty(self):
self.cache = {}
self.order_list = []
def add_cache(self, order):
self.cache[order.code] = order
if not order.code in self.order_list:
self.order_list.append(order.code)
# Will fill cache once the last cache update is greater than cache expire time
async def update_cache(self, check_itemsQuestions=False):
t = time()
to_return = False
success = True
if(t - self.lastCacheUpdate > CACHE_EXPIRE_TIME and not self.updating.locked()):
to_return = True
success = await self.fill_cache(check_itemsQuestions=check_itemsQuestions)
return (to_return, success)
def remove_cache(self, code):
if code in self.cache:
del self.cache[code]
self.order_list.remove(code)
def add_cache(self, order, cache=None, orderList=None):
# Extra params for dry runs
if(cache is None):
cache = self.cache
if(orderList is None):
orderList = self.order_list
cache[order.code] = order
if not order.code in orderList:
orderList.append(order.code)
def remove_cache(self, code, cache=None, orderList=None):
# Extra params for dry runs
if(cache is None):
cache = self.cache
if(orderList is None):
orderList = self.order_list
if code in cache:
del cache[code]
orderList.remove(code)
async def fill_cache(self):
async def fill_cache(self, check_itemsQuestions=False) -> bool:
# Check cache lock
self.updating.acquire()
start_time = time()
logger.info("[CACHE] Filling cache...")
# Index item's ids
r = await load_items()
if(not r and check_itemsQuestions):
logger.error("[CACHE] Items were not loading correctly. Aborting filling cache...")
return False
# Index questions' types
r = await load_questions()
if(not r and check_itemsQuestions):
logger.error("[CACHE] Questions were not loading correctly. Aborting filling cache...")
return False
# Load quotas
r = await load_item_quotas()
if(not r and check_itemsQuestions):
logger.error("[CACHE] Quotas were not loading correctly. Aborting filling cache...")
return False
cache = {}
orderList = []
success = True
p = 0
async with httpx.AsyncClient() as client:
try:
while 1:
p += 1
res = await client.get(join(base_url, f"orders/?page={p}"), headers=headers)
res = await pretixClient.get(f"orders/?page={p}", expectedStatusCodes=[200, 404])
if res.status_code == 404: break
# Parse order data
data = res.json()
for o in data['results']:
o = Order(o)
if o.status in ['canceled', 'expired']:
self.remove_cache(o.code)
self.remove_cache(o.code, cache=cache, orderList=orderList)
else:
self.add_cache(Order(o))
self.add_cache(Order(o), cache=cache, orderList=orderList)
self.lastCacheUpdate = time()
logger.info(f"[CACHE] Cache filled in {self.lastCacheUpdate - start_time}s.")
except Exception:
logger.error(f"[CACHE] Error while refreshing cache.\n{traceback.format_exc()}")
success = False
finally:
self.updating.release()
# Apply new cache if there were no errors
if(success):
self.cache = cache
self.order_list = orderList
# Validating rooms
rooms = list(filter(lambda o: (o.code == o.room_id), self.cache.values()))
asyncio.create_task(validate_rooms(None, rooms, self))
return success
async def get_order(self, request=None, code=None, secret=None, nfc_id=None, cached=False):
# if it's a nfc id, just retorn it
@ -233,6 +431,7 @@ class OrderManager:
if order.nfc_id == nfc_id:
return order
await self.update_cache()
# If a cached order is needed, just get it if available
if code and cached and code in self.cache and time()-self.cache[code].time < 3600:
return self.cache[code]
@ -243,27 +442,26 @@ class OrderManager:
secret = request.cookies.get("foxo_secret")
if re.match('^[A-Z0-9]{5}$', code or '') and (secret is None or re.match('^[a-z0-9]{16,}$', secret)):
print('Fetching', code, 'with secret', secret)
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'Fetching {code} with secret {secret}')
async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, f"orders/{code}/"), headers=headers)
if res.status_code != 200:
if request:
raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.")
else:
self.remove_cache(code)
return None
res = res.json()
order = Order(res)
if order.status in ['canceled', 'expired']:
self.remove_cache(order.code)
if request:
raise exceptions.Forbidden(f"Your order has been deleted. Contact support with your order identifier ({res['code']}) for further info.")
res = await pretixClient.get(f"orders/{code}/", expectedStatusCodes=None)
if res.status_code != 200:
if request:
raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.")
else:
self.add_cache(order)
if request and secret != res['secret']:
raise exceptions.Forbidden("Your session has expired due to a token change. Please check your E-Mail for an updated link!")
return order
self.remove_cache(code)
return None
res = res.json()
order = Order(res)
if order.status in ['canceled', 'expired']:
self.remove_cache(order.code)
if request:
raise exceptions.Forbidden(f"Your order has been deleted. Contact support with your order identifier ({res['code']}) for further info.")
else:
self.add_cache(order)
if request and secret != res['secret']:
raise exceptions.Forbidden("Your session has expired due to a token change. Please check your E-Mail for an updated link!")
return order

92
image_util.py Normal file
View File

@ -0,0 +1,92 @@
from config import *
from PIL import Image, ImageDraw, ImageFont
from sanic import Blueprint, exceptions
from sanic.log import logger
jobs = []
def draw_profile (source, member, position, font, size=(170, 170), border_width=5):
idraw = ImageDraw.Draw(source)
source_size = source.size
main_fill = (187, 198, 206)
propic_x = position[0]
propic_y = (source_size[1] // 2) - (size[1] // 2)
border_loc = (propic_x, propic_y, propic_x + size[0] + border_width * 2, propic_y + size[1] + border_width *2)
profile_location = (propic_x + border_width, propic_y + border_width)
propic_name_y = propic_y + size[1] + border_width + 20
border_color = SPONSORSHIP_COLOR_MAP[member['sponsorship']] if member['sponsorship'] in SPONSORSHIP_COLOR_MAP.keys() else (84, 110, 122)
# Draw border
idraw.rounded_rectangle(border_loc, border_width, border_color)
# Draw profile picture
fileName = member['propic'] or 'default.png'
with Image.open(f'res/propic/{fileName}') as to_add:
source.paste(to_add.resize (size), profile_location)
name_len = idraw.textlength(str(member['name']), font)
calc_size = 0
if name_len > size[0]:
calc_size = size[0] * 20 / name_len if name_len > size[0] else 20
font = ImageFont.truetype(font.path, calc_size)
name_len = idraw.textlength(str(member['name']), font)
name_loc = (position[0] + ((size[0] / 2) - name_len / 2), propic_name_y + (calc_size/2))
name_color = SPONSORSHIP_COLOR_MAP[member['sponsorship']] if member['sponsorship'] in SPONSORSHIP_COLOR_MAP.keys() else main_fill
idraw.text(name_loc, str(member['name']), font=font, fill=name_color)
async def generate_room_preview(request, code, room_data):
if code in jobs: raise exceptions.SanicException("Please try again later!", status_code=409)
font_path = f'res/font/NotoSans-Bold.ttf'
main_fill = (187, 198, 206)
propic_size = (170, 170)
logo_size = (200, 43)
border_width = 5
propic_gap = 50
propic_width = propic_size[0] + (border_width * 2)
propic_total_width = propic_width + propic_gap
jobs.append(code)
try:
room_data = await get_room(request, code) if not room_data else room_data
if not room_data: return
width = max([(propic_width + propic_gap) * int(room_data['capacity']) + propic_gap, 670])
height = int(width * 0.525)
font = ImageFont.truetype(font_path, 20)
# Recalculate gap
propic_gap = (width - (propic_width * int(room_data['capacity']))) // (int(room_data['capacity']) + 1)
propic_total_width = propic_width + propic_gap
# Define output image
with Image.new('RGB', (width, height), (17, 25, 31)) as source:
# Draw logo
with (Image.open('res/furizon.png') as logo, logo.resize(logo_size).convert('RGBA') as resized_logo):
source.paste(resized_logo, ((source.size[0] // 2) - (logo_size[0] // 2), 10), resized_logo)
i_draw = ImageDraw.Draw(source)
# Draw room's name
room_name_len = i_draw.textlength(room_data['name'], font)
i_draw.text((((width / 2) - room_name_len / 2), 55), room_data['name'], font=font, fill=main_fill)
# Draw members
for m in range (room_data['capacity']):
member = room_data['members'][m] if m < len(room_data['members']) else { 'name': 'Empty', 'propic': '../new.png', 'sponsorship': None }
font = ImageFont.truetype(font_path, 20)
draw_profile(source, member, (propic_gap + (propic_total_width * m), 63), font, propic_size, border_width)
source.save(f'res/rooms/{code}.jpg', 'JPEG', quality=60)
except Exception as err:
if EXTRA_PRINTS: logger.exception(str(err))
finally:
# Remove fault job
if len(jobs) > 0: jobs.pop()
if not room_data:
raise exceptions.SanicException("There's no room with that code.", status_code=404)
async def get_room (request, code):
order_data = await request.app.ctx.om.get_order(code=code)
if not order_data or not order_data.room_owner: return None
members_map = [{'name': order_data.name, 'propic': order_data.propic, 'sponsorship': order_data.sponsorship}]
for member_code in order_data.room_members:
if member_code == order_data.code: continue
member_order = await request.app.ctx.om.get_order(code=member_code)
if not member_order: continue
members_map.append ({'name': member_order.name, 'propic': member_order.propic, 'sponsorship': member_order.sponsorship})
return {'name': order_data.room_name,
'confirmed': order_data.room_confirmed,
'capacity': order_data.room_person_no,
'free_spots': order_data.room_person_no - len(members_map),
'members': members_map}

View File

@ -10,7 +10,7 @@ bp = Blueprint("karaoke", url_prefix="/manage/karaoke")
@bp.get("/admin")
async def show_songs(request, order: Order):
if order.code not in ADMINS:
if not order.isAdmin():
raise exceptions.Forbidden("Birichino")
orders = [x for x in request.app.ctx.om.cache.values() if x.karaoke_songs]
@ -28,7 +28,7 @@ async def show_songs(request, order: Order):
@bp.post("/approve")
async def approve_songs(request, order: Order):
if order.code not in ADMINS:
if not order.isAdmin():
raise exceptions.Forbidden("Birichino")
for song in request.form:
@ -44,7 +44,7 @@ async def sing_song(request, order: Order, songname):
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if order.code not in ADMINS:
if not order.isAdmin():
raise exceptions.Forbidden("Birichino")
songname = unquote(songname)

43
messages.py Normal file
View File

@ -0,0 +1,43 @@
ROOM_ERROR_TYPES = {
'room_id_mismatch': "There's a member in your room that is actually in another room, too. Please contact us as soon as possible in order to fix this issue.",
'unpaid': "Somebody in your room has not paid for their reservation, yet.",
'type_mismatch': "A member in your room has a ticket for a different type of room capacity. This happens when users swap their room types with others, without abandoning the room.",
'daily': "Some member in your room has a Daily ticket. These tickets do not include a hotel reservation.",
'capacity_mismatch': "The number of people in your room mismatches your type of ticket.",
'canceled': "Someone in your room canceled his booking and it was removed from your room."
}
EMAILS_TEXT = {
"ROOM_UNCONFIRM_TITLE": "Your room got unconfirmed",
"ROOM_UNCONFIRM_TEXT": {
'html': "Hello <b>{0}</b><br>We had to <b>unconfirm or change</b> your room <i>'{1}'</i> due to the following issues:<br></p>{2}<br><p>Please contact your room's owner or contact our support for further informations at <a href=\"https://furizon.net/contact/\"> https://furizon.net/contact/</a>.<br>Thank you.<br><br><a class=\"link\" style=\"background-color: #1095c1; color: #fff;\" href=\"https://reg.furizon.net/manage/welcome\">Manage booking</a>",
'plain': "Hello {0}\nWe had to unconfirm or change your room '{1}' due to the following issues:\n{2}\nPlease contact your room's owner or contact our support for further informations at https://furizon.net/contact/.\nThank you\n\nTo manage your booking: https://reg.furizon.net/manage/welcome"
},
"MISSING_PROPIC_TITLE": "You haven't uploaded your badges yet!",
"MISSING_PROPIC_TEXT": {
'html': "Hello <b>{0}</b><br>We noticed you still have to <b>upload {1}</b>!<br>Please enter your booking page at <a href=\"https://reg.furizon.net/manage/welcome\"> https://reg.furizon.net/manage/welcome</a> and <b>upload them</b> under the <i>\"Badge Customization\"</i> section.<br>Thank you.<br><br><a class=\"link\" style=\"background-color: #1095c1; color: #fff;\" href=\"https://reg.furizon.net/manage/welcome\">Manage booking</a>",
'plain': "Hello {0}\nWe noticed you still have to upload {1}!\nPlease enter your booking page at https://reg.furizon.net/manage/welcome and upload them under the \"Badge Customization\" section.\nThank you."
},
}
NOSECOUNT = {
'filters': {
'capacity': "Here are some furs that share your room type and don't have a confirmed room."
}
}
LOCALES = {
'shuttle_link': {
'en': 'Book now',
'it': 'Prenota ora'
},
'shuttle_link_url': {
'en': 'https://visitfiemme.regiondo.com/furizon?_ga=2.129644046.307369854.1705325023-235291123.1705325023',
'it': 'https://experience.visitfiemme.it/furizon'
}
}

89
metrics.py Normal file
View File

@ -0,0 +1,89 @@
from sanic.log import logger, logging
from logging import LogRecord
from config import *
import traceback
METRICS_REQ_NO = 0
METRICS_ERR_NO = 0 # Errors served to the clients
METRICS_PRETIX_READ = 0
METRICS_PRETIX_WRITE = 0
METRICS_PRETIX_ERRORS = 0 # Errors requesting pretix's backend
def incPretixRead():
global METRICS_PRETIX_READ
METRICS_PRETIX_READ += 1
def incPretixWrite():
global METRICS_PRETIX_WRITE
METRICS_PRETIX_WRITE += 1
def incPretixErrors():
global METRICS_PRETIX_ERRORS
METRICS_PRETIX_ERRORS += 1
def incReqNo():
global METRICS_REQ_NO
METRICS_REQ_NO += 1
def incErrorNo(): # Errors served to the clients
global METRICS_ERR_NO
METRICS_ERR_NO += 1
def getMetricsText():
global METRICS_REQ_NO
global METRICS_ERR_NO
global METRICS_PRETIX_READ
global METRICS_PRETIX_WRITE
global METRICS_PRETIX_ERRORS
out = []
out.append(f'sanic_request_count{{}} {METRICS_REQ_NO}')
out.append(f'sanic_error_count{{}} {METRICS_ERR_NO}')
out.append(f'webint_pretix_read_count{{}} {METRICS_PRETIX_READ}')
out.append(f'webint_pretix_write_count{{}} {METRICS_PRETIX_WRITE}')
out.append(f'webint_pretix_error_count{{}} {METRICS_PRETIX_ERRORS}')
return "\n".join(out)
def getRoomCountersText(request):
out = []
try:
daily = 0
counters = {}
counters_early = {}
counters_late = {}
for id in ROOM_TYPE_NAMES.keys():
counters[id] = 0
counters_early[id] = 0
counters_late[id] = 0
for order in request.app.ctx.om.cache.values():
if(order.daily):
daily += 1
else:
# Order.status must reflect the one in the Order() constructor inside ext.py
if(order.status in ["pending", "paid"] and hasattr(order, "bed_in_room") and order.bed_in_room in counters):
counters[order.bed_in_room] += 1
if(order.has_early):
counters_early[order.bed_in_room] += 1
if(order.has_late):
counters_late[order.bed_in_room] += 1
for id, count in counters.items():
out.append(f'webint_order_room_counter{{days="normal", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
for id, count in counters_early.items():
out.append(f'webint_order_room_counter{{days="early", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
for id, count in counters_late.items():
out.append(f'webint_order_room_counter{{days="late", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
out.append(f'webint_order_room_counter{{label="Daily"}} {daily}')
except Exception as e:
print(traceback.format_exc())
logger.warning("Error in loading metrics rooms")
return "\n".join(out)
class MetricsFilter(logging.Filter):
def filter(self, record : LogRecord):
return not (record.request.endswith("/manage/metrics") and record.status == 200)

50
pretixClient.py Normal file
View File

@ -0,0 +1,50 @@
import httpx
from utils import *
from config import *
from sanic.log import logger
from metrics import *
import traceback
import asyncio
async def get(url, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
async def func(client : httpx.AsyncClient) -> httpx.Request:
return await client.get(join(baseUrl, url), headers=headers)
return await doReq(url, func, incPretixRead, expectedStatusCodes, "GETing")
async def post(url, content=None, json=None, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
async def func(client : httpx.AsyncClient) -> httpx.Request:
return await client.post(join(baseUrl, url), headers=headers, content=content, json=json)
return await doReq(url, func, incPretixWrite, expectedStatusCodes, "POSTing")
async def patch(url, json, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
async def func(client : httpx.AsyncClient) -> httpx.Request:
return await client.patch(join(baseUrl, url), headers=headers, json=json)
return await doReq(url, func, incPretixWrite, expectedStatusCodes, "PATCHing")
async def doReq(url, httpxFunc, metricsFunc, expectedStatusCodes, opLogString) -> httpx.Response:
res = None
async with httpx.AsyncClient(timeout=PRETIX_REQUESTS_TIMEOUT) as client:
requests = 0
for requests in range(PRETIX_REQUESTS_MAX):
try:
metricsFunc()
res : httpx.Response = await httpxFunc(client)
if expectedStatusCodes is not None and res.status_code not in expectedStatusCodes:
incPretixErrors()
logger.warning(f"[PRETIX] Got an unexpected status code ({res.status_code}) while {opLogString} '{url}'. Allowed status codes: {', '.join(map(str, expectedStatusCodes))}")
logger.debug(f"Response: '{res.text}'")
continue
break
except Exception as e:
incPretixErrors()
logger.warning(f"[PRETIX] An error ({requests}) occurred while {opLogString} '{url}':\n{traceback.format_exc()}")
requests += 1
else:
logger.error(f"[PRETIX] Reached PRETIX_REQUESTS_MAX ({PRETIX_REQUESTS_MAX}) while {opLogString} '{url}'. Aborting")
raise httpx.TimeoutException(f"PRETIX_REQUESTS_MAX reached while {opLogString} to pretix.")
return res

View File

@ -6,9 +6,31 @@ from PIL import Image
from io import BytesIO
from hashlib import sha224
from time import time
import os
bp = Blueprint("propic", url_prefix="/manage/propic")
async def resetDefaultPropic(request, order: Order, isFursuiter, sendAnswer=True):
s = "_fursuiter" if isFursuiter else ""
if (EXTRA_PRINTS):
logger.info("Resetting {fn} picture for {orderCod}".format(fn="Badge" if not isFursuiter else "fursuit", orderCod = order.code))
with open("res/propic/default.png", "rb") as f:
data = f.read()
f.close()
convertedFilename = order.ans(f'propic{s}')
if convertedFilename is not None:
convertedFilename = f"res/propic/{convertedFilename}"
if os.path.exists(convertedFilename):
os.remove(convertedFilename) # converted file
originalFilename = f"res/propic/propic{s}_{order.code}_original"
if os.path.exists(originalFilename):
os.remove(originalFilename) # original file
await order.edit_answer_fileUpload(f'propic{s}_file', f'propic{s}_file_{order.code}_default.png', 'image/png', data)
if(sendAnswer):
await order.send_answers()
@bp.post("/upload")
async def upload_propic(request, order: Order):
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
@ -20,9 +42,11 @@ async def upload_propic(request, order: Order):
raise exceptions.BadRequest("The deadline has passed. You cannot modify the badges at this moment.")
if request.form.get('submit') == 'Delete main image':
await order.edit_answer('propic', None)
await resetDefaultPropic(request, order, False, sendAnswer=False)
await order.edit_answer('propic', None) #This MUST come after the reset default propic!
elif request.form.get('submit') == 'Delete fursuit image':
await order.edit_answer('propic_fursuiter', None)
await resetDefaultPropic(request, order, True, sendAnswer=False)
await order.edit_answer('propic_fursuiter', None) #This MUST come after the reset default propic!
else:
for fn, body in request.files.items():
if fn not in ['propic', 'propic_fursuiter']:
@ -30,15 +54,33 @@ async def upload_propic(request, order: Order):
if not body[0].body: continue
h = sha224(body[0].body).hexdigest()[:32]
# Check max file size
if EXTRA_PRINTS:
logger.debug (f"Image {fn} weight: {len(body[0].body)} bytes")
if len(body[0].body) > PROPIC_MAX_FILE_SIZE:
raise exceptions.BadRequest("File size too large for " + ("Profile picture" if fn == 'propic' else 'Fursuit picture'))
errorDetails = ''
bodyBytesBuff = None
imgBytesBuff = None
img = None
try:
img = Image.open(BytesIO(body[0].body))
bodyBytesBuff = BytesIO(body[0].body)
img = Image.open(bodyBytesBuff)
width, height = img.size
# Checking for min / max size
if width < PROPIC_MIN_SIZE[0] or height < PROPIC_MIN_SIZE[1]:
errorDetails = "Image too small [{width}x{width}] for {pfpn}".format(width=width, pfpn=("Profile picture" if fn == 'propic' else 'Fursuit picture'))
raise exceptions.BadRequest(errorDetails)
if width > PROPIC_MAX_SIZE[0] or height > PROPIC_MAX_SIZE[1]:
errorDetails = "Image too big [{width}x{width}] for {pfpn}".format(width=width, pfpn=("Profile picture" if fn == 'propic' else 'Fursuit picture'))
raise exceptions.BadRequest(errorDetails)
with open(f"res/propic/{fn}_{order.code}_original", "wb") as f:
f.write(body[0].body)
width, height = img.size
aspect_ratio = width/height
if aspect_ratio > 1:
crop_amount = (width - height) / 2
@ -48,12 +90,31 @@ async def upload_propic(request, order: Order):
img = img.crop((0, crop_amount, width, height - crop_amount))
img = img.convert('RGB')
width, height = img.size
img.thumbnail((512,512))
img.save(f"res/propic/{fn}_{order.code}_{h}.jpg")
except:
raise exceptions.BadRequest("The image you uploaded is not valid.")
imgBytesBuff = BytesIO()
img.save(imgBytesBuff, format='jpeg')
imgBytes = imgBytesBuff.getvalue()
with open(f"res/propic/{fn}_{order.code}.jpg", "wb") as f:
f.write(imgBytes)
await order.edit_answer_fileUpload(f'{fn}_file', f'{fn}_file_{order.code}.jpg', 'image/jpeg', imgBytes)
except Exception:
import traceback
if EXTRA_PRINTS: print(traceback.format_exc())
raise exceptions.BadRequest(errorDetails if errorDetails else "The image you uploaded is not valid.")
else:
await order.edit_answer(fn, f"{fn}_{order.code}_{h}.jpg")
await order.edit_answer(fn, f"{fn}_{order.code}.jpg")
if img is not None:
img.close()
if bodyBytesBuff is not None:
bodyBytesBuff.flush()
bodyBytesBuff.close()
if imgBytesBuff is not None:
imgBytesBuff.flush()
imgBytesBuff.close()
await order.send_answers()
return redirect("/manage/welcome#badge")

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 MiB

BIN
reg.furizon.net/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

BIN
reg.furizon.net/bgVert.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

21
reg.furizon.net/fz23.html Normal file
View File

@ -0,0 +1,21 @@
<html>
<head>
<title>Thanks!</title>
<style>
body {font-family: sans-serif;color:#eee;background:#222;}
main {margin: 4em auto;max-width:50em;line-height:2em;}
h1, h2 {color:#e90;}
</style>
</head>
<body>
<main>
<h1 id="a">Thanks for participating in Furizon ENABLE_JAVASCRIPT!</h1>
<p>We just came back home and we need some time to regain energy and get back to normal! The registration system is currently offline, and we are working on making it better!</p>
<h2>When will it be back online?</h2>
<p>The registration system was hosted in a server in the convention network itself. After the end of the convention, the network was disassembled. We will soon setup the server again!</p>
<h2>Can i contact you?</h2>
<p>Yes. Just write to info@furizon.net</p>
</main>
</body>
<script>document.getElementById("a").innerText = "Thanks for participating in Furizon " + (new Date().getFullYear()) + "!"</script>
</html>

147
reg.furizon.net/index.html Normal file
View File

@ -0,0 +1,147 @@
<html>
<head>
<title>Countdown to Furizon</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<meta name="viewport" content="width=500rem" />
<link href="https://fonts.bunny.net/css?family=pt-serif-caption:400" rel="stylesheet" />
<meta property="og:title" content="Furizon Overlord · Booking">
<meta property="og:description" content="Acquista i biglietti per Furizon Overlord, la convention furry Italiana.">
<style>
*{margin:0;border:0;padding:0;}
body {
font-family: 'PT Serif Caption';
background: url('bg.jpg') center center no-repeat;
backdrop-filter: blur(5px);
background-color:#111;
height:100%;
}
@media screen and (min-width: calc(100vh * 1.406)) { /*1.406 is width/height of the image*/
.containedBg {
width: 100%;
height: 100%;
background: url('bg.jpg') center center no-repeat;
background-size: contain;
}
}
@media screen and (max-width: calc(100vh * 1.406)) and (min-width: calc(100vh * 0.706)) {
.containedBg {
width: 100%;
height: 100%;
background: url('bg.jpg') right center no-repeat;
background-size: cover;
}
}
@media screen and (max-width: calc(100vh * 0.706)) {
.containedBg {
width: 100%;
height: 100%;
background: url('bgVert.jpg') center center no-repeat;
background-size: contain;
}
}
img {
display:block;
height: 6rem;
margin: 0 auto;
}
main {
padding: 1em 0;
font-size: 1.2rem;
color: #fff;
width:100%;
box-sizing:border-box;
background:rgba(0,0,0,0.5);
text-align:center;
position:absolute;
top: 50%;
transform: translateY(-50%);
}
em {
color: #FCB53F;
font-size: 0.5em;
text-decoration:none;
}
main a {
font-size:1.6em;
margin-top: 0.5em;
color: #FCB53F;
background:#000;
border-radius: 5px;
text-decoration:none;
padding: 0.3em 0.6em;
display:inline-block;
transition: all 0.2s;
}
main a:hover {
background:#FCB53F;
color: #000;
}
span {
font-size: 2em;
}
#timer {display:block;max-width:30em;margin: 0 auto;}
#clock {display:block;}
</style>
</head>
<body>
<div class="containedBg"></div>
<main>
<div id="timer">
<img src="https://furizon.net/wp-content/uploads/2022/08/Logo_base_no_sfondoo_Furizon_1-1.png" />
<h2>Overlord Registration</h2>
<span id="clock">Enjoy furizon~</span>
<a id="button" href="https://reg.furizon.net/furizon/overlord/" style="display:none">Book now!</a>
</div>
</main>
<script>
// Set the date we're counting down to
var countDownDate = 1705143600 * 1000;
var now = new Date().getTime();
if(now <= countDownDate) {
// Update the count down every 1 second
var x = setInterval(function() {
// Get today's date and time
var now = new Date().getTime();
// Find the distance between now and the count down date
var distance = countDownDate - now;
// Time calculations for days, hours, minutes and seconds
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
// Display the result in the element with id="timer"
document.getElementById("clock").innerHTML = days + "<em>d</em> " + hours + "<em>h</em> "
+ minutes + "<em>m</em> " + seconds + "<em>s</em>";
// If the count down is finished, write some text
if (distance < 0) {
clearInterval(x);
document.getElementById("button").style.display = 'block';
document.getElementById("clock").style.display = 'none';
}
}, 1000);
} else {
document.getElementById("button").style.display = 'block';
document.getElementById("clock").style.display = 'none';
}
</script>
</body>
</html>

View File

@ -0,0 +1,117 @@
<html>
<head>
<title>Countdown to Furizon</title>
<link rel="preconnect" href="https://fonts.bunny.net">
<meta name="viewport" content="width=500rem" />
<link href="https://fonts.bunny.net/css?family=pt-serif-caption:400" rel="stylesheet" />
<meta property="og:title" content="Furizon Beyond · Booking">
<meta property="og:description" content="Acquista i biglietti per Furizon Beyond, la convention furry Italiana.">
<style>
*{margin:0;border:0;padding:0;}
body {
font-family: 'PT Serif Caption';
background-image: url('https://furizon.net/wp-content/uploads/2022/12/Header-banner.jpg?id=1635');
background-repeat: no-repeat;
background-position: right;
background-size: cover;
background-color:#111;
height:100%;
}
img {
display:block;
height: 6rem;
margin: 0 auto;
}
main {
padding: 1em 0;
font-size: 1.2rem;
color: #fff;
width:100%;
box-sizing:border-box;
background:rgba(0,0,0,0.5);
text-align:center;
position:absolute;
top: 50%;
transform: translateY(-50%);
}
em {
color: #FCB53F;
font-size: 0.5em;
text-decoration:none;
}
main a {
font-size:1.6em;
margin-top: 0.5em;
color: #FCB53F;
background:#000;
border-radius: 5px;
text-decoration:none;
padding: 0.3em 0.6em;
display:inline-block;
transition: all 0.2s;
}
main a:hover {
background:#FCB53F;
color: #000;
}
#timer {display:block;max-width:30em;margin: 0 auto;}
#clock {display:block;}
</style>
</head>
<body>
<main>
<div id="timer">
<img src="https://furizon.net/wp-content/uploads/2022/08/Logo_base_no_sfondoo_Furizon_1-1.png" />
<h2>Thank you to everybody who registered for Furizon 2023!</h2>
<p>Too late to register? You can still sign up to the waiting list! We will contact you as soon as a spot is available for you~</p>
<!--<span id="clock">Enjoy furizon~</span>-->
<a id="button" href="https://reg.furizon.net/furizon/beyond/waitinglist?item=38">Join waiting list</a>
<a id="button" href="https://reg.furizon.net/furizon/beyond/redeem">Redeem code</a>
</div>
</main>
<!--<script>
// Set the date we're counting down to
var countDownDate = 1685455200 * 1000;
var now = new Date().getTime();
if(now <= countDownDate) {
// Update the count down every 1 second
var x = setInterval(function() {
// Get today's date and time
var now = new Date().getTime();
// Find the distance between now and the count down date
var distance = countDownDate - now;
// Time calculations for days, hours, minutes and seconds
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
// Display the result in the element with id="timer"
document.getElementById("clock").innerHTML = days + "<em>d</em> " + hours + "<em>h</em> "
+ minutes + "<em>m</em> " + seconds + "<em>s</em>";
// If the count down is finished, write some text
if (distance < 0) {
clearInterval(x);
document.getElementById("clock").style.display = 'none';
}
}, 1000);
}
</script>-->
</body>
</html>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View File

@ -3,4 +3,5 @@ sanic-ext
httpx
Pillow
aztec_code_generator
jinja2
Jinja2
Requests

View File

@ -94,13 +94,13 @@ function clock() {
currentTime = new Date();
let ts = currentTime.toString();
ts = ts.replace("GMT+0200 (Central European Summer Time)", "");
ts = ts.replace("2023", "<br />");
ts = ts.replace("2024", "<br />");
document.getElementById("clock").innerHTML = ts;
}, 1000);
}
function fastForward() {
currentTime = new Date("2023-05-29T18:00Z");
currentTime = new Date("2024-06-03T18:00Z");
setInterval(() => {
updateDivs(cachedData);
currentTime.setMinutes(currentTime.getMinutes()+1);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

BIN
res/font/NotoSans-Bold.ttf Normal file

Binary file not shown.

1
res/icons/book-plus.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 19C13 20.1 13.3 21.12 13.81 22H6C4.89 22 4 21.11 4 20V4C4 2.9 4.89 2 6 2H7V9L9.5 7.5L12 9V2H18C19.1 2 20 2.89 20 4V13.09C19.67 13.04 19.34 13 19 13C15.69 13 13 15.69 13 19M20 18V15H18V18H15V20H18V23H20V20H23V18H20Z" /></svg>

After

Width:  |  Height:  |  Size: 297 B

1
res/icons/bus.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18,11H6V6H18M16.5,17A1.5,1.5 0 0,1 15,15.5A1.5,1.5 0 0,1 16.5,14A1.5,1.5 0 0,1 18,15.5A1.5,1.5 0 0,1 16.5,17M7.5,17A1.5,1.5 0 0,1 6,15.5A1.5,1.5 0 0,1 7.5,14A1.5,1.5 0 0,1 9,15.5A1.5,1.5 0 0,1 7.5,17M4,16C4,16.88 4.39,17.67 5,18.22V20A1,1 0 0,0 6,21H7A1,1 0 0,0 8,20V19H16V20A1,1 0 0,0 17,21H18A1,1 0 0,0 19,20V18.22C19.61,17.67 20,16.88 20,16V6C20,2.5 16.42,2 12,2C7.58,2 4,2.5 4,6V16Z" /></svg>

After

Width:  |  Height:  |  Size: 466 B

1
res/icons/calendar.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M580-240q-42 0-71-29t-29-71q0-42 29-71t71-29q42 0 71 29t29 71q0 42-29 71t-71 29ZM200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z"/></svg>

After

Width:  |  Height:  |  Size: 386 B

1
res/icons/close.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" /></svg>

After

Width:  |  Height:  |  Size: 188 B

1
res/icons/delete.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>

After

Width:  |  Height:  |  Size: 300 B

1
res/icons/door_open.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M440-440q17 0 28.5-11.5T480-480q0-17-11.5-28.5T440-520q-17 0-28.5 11.5T400-480q0 17 11.5 28.5T440-440ZM280-120v-80l240-40v-445q0-15-9-27t-23-14l-208-34v-80l220 36q44 8 72 41t28 77v512l-320 54Zm-160 0v-80h80v-560q0-34 23.5-57t56.5-23h400q34 0 57 23t23 57v560h80v80H120Zm160-80h400v-560H280v560Z"/></svg>

After

Width:  |  Height:  |  Size: 399 B

BIN
res/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
res/icons/menu.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /></svg>

After

Width:  |  Height:  |  Size: 125 B

1
res/icons/pencil.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/></svg>

After

Width:  |  Height:  |  Size: 310 B

1
res/icons/share.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19A2.92,2.92 0 0,0 18,16.08Z" /></svg>

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,21 @@
function confirmAction (intent, sender) {
if (['propicReminder'].includes (intent) == false) return
let href = sender.getAttribute('action')
let intentTitle = document.querySelector("#intentText")
let intentDescription = document.querySelector("#intentDescription")
let intentEditPanel = document.querySelector("#intentEditPanel")
let intentFormAction = document.querySelector("#intentFormAction")
let intentSend = document.querySelector("#intentSend")
// Resetting ui
intentFormAction.setAttribute('method', 'GET')
intentEditPanel.style.display = 'none';
intentDescription.innerText = sender.title;
intentFormAction.setAttribute('action', href)
switch (intent){
case 'propicReminder':
intentTitle.innerText = "Send missing badge reminders";
intentSend.innerText = sender.innerText;
break;
}
document.getElementById('modalRoomconfirm').setAttribute('open', 'true');
}

23
res/scripts/base.js Normal file
View File

@ -0,0 +1,23 @@
function onScrollNav () {
if (Number(window.currentScroll) == 0) {
window.currentScroll = window.scrollY || document.documentElement.scrollTop;
return;
}
let newOffset = window.scrollY || document.documentElement.scrollTop;
document.getElementById('topbar').classList.toggle('closed', newOffset > window.currentScroll);
window.currentScroll = newOffset <= 0 ? 0 : newOffset;
}
document.getElementById('mobileMenu').addEventListener('click', function (e) {
menuClick (e.target);
e.stopPropagation();
});
function menuClick (e, force){
let menuItem = e.closest('img');
let isOpen = false;
isOpen = document.querySelector('nav div.navbar-container').classList.toggle('open', force);
menuItem.setAttribute('src', isOpen ? '/res/icons/close.svg' : '/res/icons/menu.svg');
}

View File

@ -0,0 +1,31 @@
function confirmAction (intent, sender) {
if (['rename', 'unconfirm', 'delete'].includes (intent) == false) return
let href = sender.getAttribute('action')
let intentTitle = document.querySelector("#modalOrderEditDialog #intentText")
let intentEdit = document.querySelector("#modalOrderEditDialog #intentRename")
let intentEditPanel = document.querySelector("#modalOrderEditDialog #intentEditPanel")
let intentFormAction = document.querySelector("#intentFormAction")
let intentSend = document.querySelector("#modalOrderEditDialog #intentSend")
// Resetting ui
intentEdit.removeAttribute('required')
intentEdit.removeAttribute('minlength')
intentFormAction.setAttribute('method', 'GET')
intentEditPanel.style.display = 'none';
intentTitle.innerText = intent + ' room'
intentFormAction.setAttribute('action', href)
switch (intent){
case 'rename':
intentEditPanel.style.display = 'block';
intentEdit.setAttribute('required', true)
intentEdit.setAttribute('minlength', 4)
intentFormAction.setAttribute('method', 'POST')
document.getElementById("intentRename").value = sender.parentElement.parentElement.querySelector("span").innerText;
break
case 'unconfirm':
break
case 'delete':
break
}
document.getElementById('modalOrderEditDialog').setAttribute('open', 'true');
}

View File

@ -0,0 +1,213 @@
var draggingData = {
id: 0,
roomTypeId: 0,
parentRoomId: 0
}
var allowRedirect = false;
function initObjects (){
draggables = document.querySelectorAll("div.grid.people div.edit-drag");
rooms = document.querySelectorAll("main.container>div.room");
Array.from(draggables).forEach(element => {
element.addEventListener('dragstart', dragStart);
element.addEventListener('dragend', dragEnd);
});
Array.from(rooms).forEach(room => {
room.addEventListener('dragenter', dragEnter)
room.addEventListener('dragover', dragOver);
room.addEventListener('dragleave', dragLeave);
room.addEventListener('drop', drop);
});
}
/**
*
* @param {DragEvent} e
*/
function dragStart(e) {
element = e.target;
room = element.closest('div.room')
setData(element.id, element.getAttribute('room-type'), room.id)
e.dataTransfer.effectAllowed = 'move';
setTimeout(()=>toggleRoomSelection(true), 0);
}
function dragEnd(e) {
toggleRoomSelection(false);
resetData ();
e.stopPropagation();
}
function dragEnter(e) {
e.preventDefault();
e.target.classList.add('drag-over');
checkDragLocation (getData(), e.target);
e.stopPropagation();
}
function dragOver(e) {
e.preventDefault();
e.target.classList.add('drag-over');
checkDragLocation (getData(), e.target)
e.stopPropagation();
}
/**
*
* @param {Element} target
*/
function checkDragLocation (data, target) {
let toReturn = true;
const isInfinite = target.getAttribute("infinite");
const maxSizeReached = target.getAttribute("current-size") >= target.getAttribute("room-size");
const roomTypeMismatch = data.roomTypeId !== target.getAttribute("room-type");
if (!isInfinite && (maxSizeReached || roomTypeMismatch)) {
target.classList.add('drag-forbidden');
toReturn = false;
} else {
target.classList.remove('drag-forbidden');
}
return toReturn;
}
function dragLeave(e) {
e.target.classList.remove('drag-over');
e.target.classList.remove('drag-forbidden');
}
function drop(e) {
e.target.classList.remove('drag-over');
toggleRoomSelection(false);
if (checkDragLocation(getData(), e.target) === true) {
const data = getData();
let item = document.getElementById (data.id)
let oldParent = document.getElementById (data.parentRoomId)
let newParent = e.target;
if (moveToRoom (data.id, data.parentRoomId.replace('room-',''), newParent.id.replace('room-','')) === true) {
let newParentContainer = newParent.querySelector('div.grid.people')
newParentContainer.appendChild (item);
let oldParentQty = parseInt(oldParent.getAttribute("current-size")) - 1;
let newParentQty = parseInt(newParent.getAttribute("current-size")) + 1;
let newParentCapacity = parseInt(newParent.getAttribute("room-size"));
oldParent.setAttribute("current-size", oldParentQty);
newParent.setAttribute("current-size", newParentQty);
oldParent.classList.remove('complete');
if (newParentCapacity == newParentQty) newParent.classList.add('complete');
// if owner of room is being moved, assign a new owner
if (data.parentRoomId.replace('room-','') == data.id) {
// find first owner
if (model[data.id][toAdd] && model[data.id][toAdd].length <= 0) return;
newOwner = model[data.id][toAdd][0]
changeOwner (data.id, newOwner)
oldParent.id = "room-" + newOwner
}
}
}
}
function toggleRoomSelection(newStatus) {
rooms = document.querySelectorAll("div.room");
Array.from(rooms).forEach(room=>{
room.classList.toggle('interactless', newStatus);
room.classList.remove('drag-over');
room.classList.remove('drag-forbidden');
})
}
function setData (id, roomType, parentRoomId) {
draggingData.id = id;
draggingData.roomTypeId = roomType;
draggingData.parentRoomId = parentRoomId;
}
function resetData (){ setData(0, 0, 0); }
function getData () { return draggingData; }
// This default onbeforeunload event
window.onbeforeunload = function(){
if (!allowRedirect) return "Any changes to the rooms will be discarded."
}
/* Model managing */
var model = saveData;
const toAdd = "to_add";
function moveToRoom (order, from, to){
if (!model) { console.error("Model is null", order, from, to); return false; }
if (!model[from]) { console.error("Parent is null", order, from, to); return false; }
if (!model[to]) { console.error("Destination is null", order, from, to); return false; }
if (!model[from][toAdd] || !model[from][toAdd].includes(order)) { console.error("Order is not in parent", order, from, to); return false; }
if (!model[to][toAdd]) model[to][toAdd] = [];
// Delete order from the original room
model[from][toAdd] = model[from][toAdd].filter (itm=> itm !== order)
// Add it to the destination room
model[to][toAdd].push (order);
return true;
}
function changeOwner (from, to){
if (!model) { console.error("Model is null", from, to); return false; }
if (!model[from]) { console.error("Parent is null", from, to); return false; }
if (model[to]) { console.error("Destination already exist", from, to); return false; }
model[to] = {...model[from]}
delete model[from]
}
function onSave (){
if (model['infinite'] && model['infinite'][toAdd] && model['infinite'][toAdd].length > 0) {
setTimeout(()=>{
let roomItem = document.querySelector("#room-infinite");
roomItem.scrollIntoView();
roomItem.classList.add('drag-forbidden');
setTimeout(()=>roomItem.classList.remove('drag-forbidden'), 3000);
}, 100);
} else {
document.getElementById('modalConfirmDialog').setAttribute('open', 'true');
}
}
/**
*
* @param {Element} element
*/
function submitData (element){
if (element.ariaDisabled) return;
element.ariaDisabled = true;
element.setAttribute("aria-busy", true);
document.querySelector("#modalClose").setAttribute("disabled", true);
document.querySelector("#modalClose").style.display = 'none';
// Create request
const xhr = new XMLHttpRequest();
xhr.open('POST', '/manage/admin/room/wizard/submit', true);
xhr.withCredentials = true;
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
let popoverText = document.querySelector("#popover-status-text");
let popoverStatus = document.querySelector("#popover-status");
popoverStatus.classList.remove('status-error');
popoverStatus.classList.remove('status-success');
if (xhr.status === 200) {
// Handle correct redirect
popoverText.innerText = "Changes applied successfully. Redirecting..."
popoverStatus.classList.add('status-success');
} else {
// Handle errors
let error = xhr.statusText;
popoverText.innerText = "Could not apply changes: " + error;
console.error('Error submitting data:', error);
popoverStatus.classList.add('status-error');
}
popoverStatus.showPopover();
allowRedirect = true;
setTimeout(()=>window.location.assign('/manage/admin'), 3000);
}
};
xhr.send(JSON.stringify(model));
}
initObjects ();

53
res/styles/admin.css Normal file
View File

@ -0,0 +1,53 @@
div.room-actions {
container-name: room-actions;
float: right;
}
div.admin-actions-header {
container-name: room-actions;
float: unset !important;
max-height: 2rem;
margin: 1rem 0px;
}
div.admin-actions-header img {
max-height: 1.5rem;
}
div.room-actions > a {
background-color: var(--card-background-color);
font-size: 12pt;
padding: 0.7rem;
border-radius: 1rem;
}
div.room-actions > a:hover {
background-color: var(--primary-focus);
}
div.room-actions > a.act-del:hover {
background-color: var(--del-color);
}
/* Spinning animation */
@keyframes spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
}
h3:has(.spin) {
overflow: hidden;
}
.spin {
animation-name: spin;
animation-duration: 500ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
max-height: 32px;
}

54
res/styles/base.css Normal file
View File

@ -0,0 +1,54 @@
/* Other blocks' styles */
@import url('propic.css');
@import url('admin.css');
@import url('room.css');
@import url('navbar.css');
a[role=button] {
margin: 0.25em 0;
}
summary:has(span.status) {
background-color: #ffaf0377;
}
.rainbow-text {
background-image: repeating-linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff);
background-size: 2000% 2000%;
color: transparent;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: rainbow 4s linear infinite;
}
@keyframes rainbow {
0% { background-position:0% 0%; }
100% { background-position:57.75% 0%; }
}
/* Popover */
*[popover]:popover-open {
border-radius: var(--border-radius);
border: 1px solid #fff;
backdrop-filter: blur(10px);
padding: 1rem;
}
/* Dark theme */
@media only screen and (prefers-color-scheme: dark) {
.icon {filter: invert(1);}
}
/* Mobile only */
@media only screen and ((max-width: 500px) or (hover: none)) {
.propic-border.blurred {
display: none;
}
}
@media only screen and (prefers-reduced-motion) {
.propic-border.blurred {
display: none;
}
}

91
res/styles/navbar.css Normal file
View File

@ -0,0 +1,91 @@
nav#topbar.closed {
top: -10rem;
}
nav#topbar {
background-color :var(--card-background-color);
width: 100vw;
position: sticky;
display: inline-block;
z-index: 9999;
padding: 0.2rem 0.7em;
margin: 0;
white-space: nowrap;
top: 0rem;
transition: top 300ms;
line-height: 2em;
max-width:100%;
overflow-x: hidden;
}
nav#topbar a {
display: inline-block;
padding: 0 0.6em;
line-height: 2em;
margin: 0;
white-space: nowrap;
vertical-align: middle;
}
nav#topbar .navbar-propic {
margin-right: 0.3rem;
border-radius: 0.2rem;
vertical-align: sub;
}
nav#topbar a.align-right {
float: right;
}
nav img {
height: 1.2em;
display: inline;
vertical-align:middle;
box-sizing: border-box;
}
nav a#mobileMenu {
display: none;
}
nav .navbar-container {
overflow: hidden;
}
@media only screen and (max-width: 924px) {
nav#topbar::before {
transition-delay: 200ms;
transition-property: background-image;
}
nav#topbar:has(.navbar-container:not(.open))::before{
background-image: url('/res/furizon.png');
background-size: contain;
background-position: 50%;
background-origin: content-box;
}
nav#topbar.closed {
top: -100rem;
}
nav#topbar a#mobileMenu {
display: block;
max-width: 3rem;
}
nav#topbar .navbar-container {
transition: max-height 200ms;
display: flex;
flex-direction: column;
}
nav#topbar .navbar-container:not(.open) {
max-height: 0rem;
}
nav#topbar .navbar-container.open {
max-height: 30rem;
}
}

113
res/styles/propic.css Normal file
View File

@ -0,0 +1,113 @@
svg.propic-border-filter {
display: none;
}
.propic-container {
max-width:7em;
margin:0 auto;
position: relative;
}
.propic-container img {
display: block;
}
.propic-container .absolute {
position: absolute;
}
.propic-filler {
width: 100%;
border-radius: 0.4em;
margin: 0 auto;
border: 4px solid #546e7a;
opacity: 0;
}
.propic {
width:calc(100% - 4mm);
border-radius:0.4em;
margin:2mm;
z-index: 2;
}
.propic-border {
top: 0%;
z-index: 0;
min-width: 100%;
min-height: 100%;
border-radius:0.6em;
border: none !important;
overflow: hidden;
}
.propic-border-animation {
min-width: 200%;
min-height: 200%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: 0% 0%;
}
.propic-border.blurred {
filter: blur(20px);
z-index: 1;
}
.propic-border.propic-animated-super .propic-border-animation {
background: linear-gradient(90deg, #FA5E00 20%, #FAAA00 40%, #FB8C00 60%, #FA3200 80%, #FAC800 100%);
animation: border-animation 2.5s linear 0ms infinite;
}
.propic-border.propic-animated-normal .propic-border-animation {
background: linear-gradient(90deg, #6124AB 20%, #AB248F 40%, #8E24AA 60%, #3524AB 80%, #AB243E 100%);
animation: border-animation 2.5s linear 0ms infinite;
}
@keyframes border-animation {
0% {
rotate: 0deg;
}
100% {
rotate: 360deg;
}
}
.propic-flag {
z-index: 4;
max-width: 2em;
border-radius:2px;
right: calc(0% - 0.3em);
bottom: -0.1em;
}
.propic-super {
border:4px solid #fb8c00;
}
.propic-normal {
border:4px solid #8e24aa;
}
.propic-base {
border:4px solid #546e7a;
}
.control-login-as:hover::before{
background-color: #2f4e5cc5;
position: absolute;
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
content: 'Enter as';
color: white;
text-decoration: none;
text-align: center;
z-index: 3;
border-radius: 0.6rem;
}

20
res/styles/room.css Normal file
View File

@ -0,0 +1,20 @@
span.nsc-room-counter {
font-size: medium;
color: var(--color);
}
#intentFormAction #intentText {
text-transform: capitalize;
}
#room a[role=button] {
display: inline-flex;
justify-content: space-around;
align-items: center;
}
@media only screen and (max-width: 500px) {
div.room-actions a>span {
display: none;
}
}

80
res/styles/wizard.css Normal file
View File

@ -0,0 +1,80 @@
div.grid.people div.edit-disabled {
pointer-events: none;
filter: grayscale(1);
user-select: none;
cursor: not-allowed;
}
div.grid.people div.edit-drag>div.propic-container {
pointer-events: none;
}
div.drag-over {
border-color: #000;
border-style: dashed;
}
div.drag-forbidden {
border-color: #c92121aa;
}
div.interactless > * {
pointer-events: none;
}
div.room.complete {
border-color: #21c929aa;
border-style: solid;
}
div.room {
border-radius: var(--border-radius);
border: 1px solid transparent;
margin-bottom: var(--spacing);
}
div.room > h4 {
user-select: none;
margin-left: 1.6rem;
}
div.room:nth-child(2n) {
background-color: #ffffff55;
}
div.room:nth-child(2n) {
background-color: #cccccc55;
}
.align-right {
float: right;
}
.status-success {
background-color: #2e9147aa;
}
.status-error {
background-color: #912e2eaa;
}
/* Dark theme */
@media only screen and (prefers-color-scheme: dark) {
div.drag-over {
border-color: #fff;
border-style: dashed;
}
div.drag-forbidden {
border-color: #c92121aa;
}
div.room {
background-color: #16161655;
}
div.room:nth-child(2n) {
background-color: #20202055;
}
}

131
room.py
View File

@ -3,6 +3,9 @@ from sanic import Blueprint, exceptions
from random import choice
from ext import *
from config import headers
import os
from image_util import generate_room_preview, get_room
from utils import confirm_room_by_order
bp = Blueprint("room", url_prefix="/manage/room")
@ -18,6 +21,9 @@ async def room_create_post(request, order: Order):
if order.room_id:
error = "You are already in another room. You need to delete (if it's yours) or leave it before creating another."
if order.daily:
raise exceptions.BadRequest("You cannot create a room if you have a daily ticket!")
if not error:
await order.edit_answer('room_name', name)
@ -35,6 +41,9 @@ async def room_create(request, order: Order):
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if order.daily:
raise exceptions.BadRequest("You cannot create a room if you have a daily ticket!")
tpl = request.app.ctx.tpl.get_template('create_room.html')
return html(tpl.render(order=order))
@ -43,7 +52,7 @@ async def delete_room(request, order: Order):
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if order.room_id != order.code:
if not order.room_owner:
raise exceptions.BadRequest("You are not allowed to delete room of others.")
if order.ans('room_confirmed'):
@ -57,6 +66,7 @@ async def delete_room(request, order: Order):
await order.edit_answer('room_members', None)
await order.edit_answer('room_secret', None)
await order.send_answers()
remove_room_preview (order.code)
return redirect('/manage/welcome')
@bp.post("/join")
@ -70,6 +80,9 @@ async def join_room(request, order: Order):
if order.room_id:
raise exceptions.BadRequest("You are in another room already. Why would you join another?")
if order.daily:
raise exceptions.BadRequest("You cannot join a room if you have a daily ticket!")
code = request.form.get('code').strip()
room_secret = request.form.get('room_secret').strip()
@ -87,6 +100,9 @@ async def join_room(request, order: Order):
if room_owner.room_confirmed:
raise exceptions.BadRequest("The room you're trying to join has been confirmed already")
if room_owner.bed_in_room != order.bed_in_room:
raise exceptions.BadRequest("This room's ticket is of a different type than yours!")
#if room_owner.pending_roommates and (order.code in room_owner.pending_roommates):
#raise exceptions.BadRequest("What? You should never reach this check, but whatever...")
@ -100,7 +116,7 @@ async def join_room(request, order: Order):
await room_owner.edit_answer('pending_roommates', ','.join(pending_roommates))
await room_owner.send_answers()
remove_room_preview (code)
return redirect('/manage/welcome')
@bp.route("/kick/<code>")
@ -123,7 +139,7 @@ async def kick_member(request, code, order: Order):
await order.send_answers()
await to_kick.send_answers()
remove_room_preview (order.code)
return redirect('/manage/welcome')
@bp.route("/renew_secret")
@ -164,6 +180,9 @@ async def cancel_request(request, order: Order):
async def approve_roomreq(request, code, order: Order):
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if not order.room_owner:
raise exceptions.BadRequest("You are not the owner of the room!")
if not code in order.pending_roommates:
raise exceptions.BadRequest("You cannot accept people that didn't request to join your room")
@ -187,7 +206,7 @@ async def approve_roomreq(request, code, order: Order):
await pending_member.send_answers()
await order.send_answers()
remove_room_preview(order.code)
return redirect('/manage/welcome')
@bp.route("/leave")
@ -211,13 +230,16 @@ async def leave_room(request, order: Order):
await room_owner.send_answers()
await order.send_answers()
remove_room_preview (order.room_id)
return redirect('/manage/welcome')
@bp.route("/reject/<code>")
async def reject_roomreq(request, code, order: Order):
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if not order.room_owner:
raise exceptions.BadRequest("You are not the owner of the room!")
if not code in order.pending_roommates:
raise exceptions.BadRequest("You cannot reject people that didn't request to join your room")
@ -240,6 +262,32 @@ async def reject_roomreq(request, code, order: Order):
await order.send_answers()
return redirect('/manage/welcome')
@bp.post("/rename")
async def rename_room(request, order: Order):
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if not order.room_owner:
raise exceptions.BadRequest("You are not the owner of the room!")
if not order.room_id:
raise exceptions.BadRequest("Try joining a room before renaming it.")
if order.room_confirmed:
raise exceptions.BadRequest("You can't rename a confirmed room!")
if order.room_id != order.code:
raise exceptions.BadRequest("You are not allowed to rename rooms of others.")
name = request.form.get('name')
if len(name) > 64 or len(name) < 4:
raise exceptions.BadRequest("Your room name is invalid. Please try another one.")
await order.edit_answer("room_name", name)
await order.send_answers()
remove_room_preview(order.code)
return redirect('/manage/welcome')
@bp.route("/confirm")
async def confirm_room(request, order: Order, quotas: Quotas):
@ -252,54 +300,31 @@ async def confirm_room(request, order: Order, quotas: Quotas):
if order.room_id != order.code:
raise exceptions.BadRequest("You are not allowed to confirm rooms of others.")
if quotas.get_left(len(order.room_members)) == 0:
raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
# This is not needed anymore you buy tickets already
#if quotas.get_left(len(order.room_members)) == 0:
# raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
room_members = []
for m in order.room_members:
if m == order.code:
res = order
else:
res = await request.app.ctx.om.get_order(code=m)
if res.room_id != order.code:
raise exceptions.BadRequest("Please contact support: some of the members in your room are actually somewhere else")
if res.status != 'paid':
raise exceptions.BadRequest("Somebody hasn't paid.")
room_members.append(res)
for rm in room_members:
await rm.edit_answer('room_id', order.code)
await rm.edit_answer('room_confirmed', "True")
await rm.edit_answer('pending_roommates', None)
await rm.edit_answer('pending_room', None)
thing = {
'order': order.code,
'addon_to': order.position_positionid,
'item': ITEM_IDS['room'],
'variation': ROOM_MAP[len(room_members)]
}
async with httpx.AsyncClient() as client:
res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing)
if res.status_code != 201:
raise exceptions.BadRequest("Something has gone wrong! Please contact support immediately")
'''for rm in room_members:
if rm.code == order.code: continue
thing = {
'order': rm.code,
'addon_to': rm.position_positionid,
'item': ITEM_IDS['room'],
'variation': ROOM_MAP[len(room_members)]
}
res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing) '''
for rm in room_members:
await rm.send_answers()
confirm_room_by_order(order, request)
return redirect('/manage/welcome')
async def get_room_with_order (request, code):
order_data = await request.app.ctx.om.get_order(code=code)
if not order_data or not order_data.room_owner: return None
def remove_room_preview(code):
preview_file = f"res/rooms/{code}.jpg"
try:
if os.path.exists(preview_file): os.remove(preview_file)
except Exception as ex:
if (EXTRA_PRINTS): logger.exception(str(ex))
@bp.route("/view/<code>")
async def get_view(request, code):
room_file_name = f"res/rooms/{code}.jpg"
room_data = await get_room(request, code)
if not room_data: raise exceptions.NotFound("No room was found with that code.")
if not os.path.exists(room_file_name):
await generate_room_preview(request, code, room_data)
tpl = request.app.ctx.tpl.get_template('view_room.html')
return html(tpl.render(preview=room_file_name, room_data=room_data))

3
startup.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
python3 app.py 2>&1 | tee -a log.txt

View File

@ -1,12 +1,41 @@
from sanic.response import html
from sanic import Blueprint
from sanic import Blueprint, Request
from messages import NOSECOUNT
from ext import *
bp = Blueprint("stats", url_prefix="/manage")
@bp.route("/nosecount")
async def nose_count(request, order: Order):
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e']}
@bp.route("/sponsorcount")
async def sponsor_count(request, order: Order):
await request.app.ctx.om.update_cache()
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: x[1].ans('fursona_name')) if value.status not in ['c', 'e']}
tpl = request.app.ctx.tpl.get_template('nosecount.html')
tpl = request.app.ctx.tpl.get_template('sponsorcount.html')
return html(tpl.render(orders=orders, order=order))
def calc_filter(orders: dict, filter_cmd: str, order: Order) -> tuple[dict, str]:
if not filter_cmd or len(filter_cmd) == 0 or not orders or len(orders.keys()) == 0: return
if filter_cmd.lower() == "capacity":
return {key:value for key,value in orders.items() if (not value.room_confirmed and value.bed_in_room == order.bed_in_room)}, NOSECOUNT['filters'][filter_cmd.lower()]
else:
return None, None
@bp.route("/nosecount")
async def nose_count(request: Request, order: Order):
await request.app.ctx.om.update_cache()
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e']}
filtered: dict = None
filter_message: str = None
for query in request.query_args:
if query[0] == "filter" and order:
filtered, filter_message = calc_filter(orders, query[1], order) if filter else None
tpl = request.app.ctx.tpl.get_template('nosecount.html')
return html(tpl.render(orders=orders, order=order, filtered=filtered, filter_header=filter_message))
@bp.route("/fursuitcount")
async def fursuit_count(request, order: Order):
await request.app.ctx.om.update_cache()
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: x[1].ans('fursona_name')) if value.status not in ['c', 'e']}
tpl = request.app.ctx.tpl.get_template('fursuitcount.html')
return html(tpl.render(orders=orders, order=order))

78
stuff/logOrders.py Normal file
View File

@ -0,0 +1,78 @@
from config import *
import requests
import datetime
import time
ROOM_CAPACITY_MAP = {
0: 0,
# SACRO CUORE
83: 11,
67: 50,
68: 45,
69: 84,
70: 10,
# OVERFLOW 1
75: 50
}
def ans(data, name):
for p in data['positions']:
for a in p['answers']:
if a.get('question_identifier', None) == name:
if a['answer'] in ['True', 'False']:
return bool(a['answer'] == 'True')
return a['answer']
return None
def getOrders():
ret = []
p = 0
while 1:
p += 1
res = requests.get(f"{base_url_event}orders/?page={p}", headers=headers)
if res.status_code == 404: break
data = res.json()
for o in data['results']:
roomType = 0
for pos in o['positions']:
if pos['item'] == ITEMS_ID_MAP['bed_in_room']:
roomType = pos['variation']
ret.append({"code": o['code'], "fname": ans(o, 'fursona_name'), "rType": roomType, "date": o['datetime']})
return ret
ordersCode = set()
ordersTime = set()
ordersFName = set()
while True:
#try:
newOrders = getOrders()
shouldSleep = True
for o in newOrders:
if o['code'] not in ordersCode and not o['date'] in ordersTime and not o['fname'] in ordersFName:
remainingInRoomType = ROOM_CAPACITY_MAP[o['rType']]
remainingInRoomType -= 1
ROOM_CAPACITY_MAP[o['rType']] = remainingInRoomType
roomCapacitiesStr = ", ".join(str(x).rjust(2, "0") for x in ROOM_CAPACITY_MAP.values())
#dateStr = datetime.datetime.now().isoformat()
print(f"[{o['date']}] {len(ordersCode)} - [{o['code']}] New order! FursonaName: {o['fname'].ljust(24)} - Room capacities: {roomCapacitiesStr}")
shouldSleep = False
time.sleep(0.05)
ordersCode.add(o['code'])
ordersTime.add(o['date'])
ordersFName.add(o['fname'])
#except:
# print("Exception occurred!")
# pass
if shouldSleep:
time.sleep(1)

7
stuff/rclone/clone.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
/usr/bin/tar -czvf /tmp/backupRclone.tar.gz /home/ /root/ /etc/ /var/backups/ /var/log/ /var/mail/ /var/pretix-data/ /var/prometheus-data/ /var/spool/ /var/www/ /var/lib/grafana/ /var/lib/redis/
/usr/bin/rclone sync /tmp/backupRclone.tar.gz webservice:/backups/backupRclone.tar.gz -P --stats=1s --bwlimit 15M
/usr/bin/rm /tmp/backupRclone.tar.gz

View File

@ -0,0 +1,8 @@
#!/bin/bash
source /root/.profile
/usr/bin/tar -czf /tmp/backupRclone.tar.gz /home/ /root/ /etc/ /var/backups/ /var/log/ /var/mail/ /var/pretix-data/ /var/prometheus-data/ /var/spool/ /var/www/ /var/lib/grafana/ /var/lib/redis/
/usr/bin/rclone sync /tmp/backupRclone.tar.gz webservice:/backups/backupRclone.tar.gz --bwlimit 15M
/usr/bin/rm /tmp/backupRclone.tar.gz

44
stuff/runBackup.py Normal file
View File

@ -0,0 +1,44 @@
#!/usr/bin/python
from os import listdir, remove
from os.path import isfile, join
import datetime
import subprocess
PRETIX_BACKUP = False
WEBINT_BACKUP = False
BACKUP_DIR_PRETIX = "/home/pretix/backups/"
BACKUP_DIR_WEBINT = "/home/webint/backups/"
MAX_FILE_NO = 14
COMMAND_PRETIX_POSTGRES = "pg_dump -F p pretix | gzip > %s" # Restore with psql -f %s
COMMAND_PRETIX_DATA = "tar -cf %s /var/pretix-data" # Restore with tar -xvf %s. To make .secret readable I used setfacl -m u:pretix:r /var/pretix-data/.secret
COMMAND_WEBINT = "tar -cf %s /home/webint/furizon_webint" # Restore with tar -xvf %s
def deleteOlder(path : str, prefix : str, postfix : str):
backupFileNames = sorted([f for f in listdir(path) if (isfile(join(path, f)) and f.startswith(prefix) and f.endswith(postfix))])
while(len(backupFileNames) > MAX_FILE_NO):
print(f"Removing {backupFileNames[0]}")
remove(join(path, backupFileNames[0]))
backupFileNames.pop(0)
def genFileName(prefix : str, postfix : str):
return prefix + "_" + datetime.datetime.now(datetime.UTC).strftime('%Y%m%d-%H%M%S') + "_" + postfix
def runBackup(prefix : str, postfix : str, path : str, command : str):
deleteOlder(path, prefix, postfix)
name = join(path, genFileName(prefix, postfix))
process = subprocess.Popen(command % name, shell=True)
process.wait()
if(PRETIX_BACKUP):
runBackup("pretix_postres", "backup.sql.gz", join(BACKUP_DIR_PRETIX, "postgres"), COMMAND_PRETIX_POSTGRES)
runBackup("pretix_data", "backup.tar.gz", join(BACKUP_DIR_PRETIX, "data"), COMMAND_PRETIX_DATA)
if(WEBINT_BACKUP):
runBackup("webint_full", "backup.tar.gz", BACKUP_DIR_WEBINT, COMMAND_WEBINT)

91
stuff/systemMetrics.py Normal file
View File

@ -0,0 +1,91 @@
import psutil
import time
import sys
from sanic import Sanic
from sanic import response as res
DEV_MODE = False
ACCESS_LOG = False
app = Sanic(__name__)
@app.route("/")
async def main(req):
return res.text("I\'m a teapot", status=418)
@app.route("/metrics")
async def metrics(req):
out = []
cpus = psutil.cpu_percent(percpu=True)
totalCpu = 0
for i in range(len(cpus)):
out.append(f'monitor_cpu_core_usage_percent{{core="{i}"}} {cpus[i]}')
totalCpu += cpus[i]
out.append(f'monitor_cpu_core_usage_percent{{core="avg"}} {"%.2f" % (totalCpu / len(cpus))}')
diskIo = psutil.disk_io_counters(nowrap=True)
out.append(f'monitor_diskio{{value="read_count"}} {diskIo.read_count}')
out.append(f'monitor_diskio{{value="write_count"}} {diskIo.write_count}')
out.append(f'monitor_diskio{{value="read_bytes"}} {diskIo.read_bytes}')
out.append(f'monitor_diskio{{value="write_bytes"}} {diskIo.write_bytes}')
out.append(f'monitor_diskio{{value="read_time"}} {diskIo.read_time}')
out.append(f'monitor_diskio{{value="write_time"}} {diskIo.write_time}')
disks = psutil.disk_partitions()
for disk in disks:
try:
dUsage = psutil.disk_usage(disk.mountpoint)
out.append(f'monitor_disk_usage_percent{{partition="{disk.mountpoint}"}} {dUsage.percent}')
except:
pass
mem = psutil.virtual_memory()
out.append(f'monitor_memory{{value="total"}} {mem.total}')
out.append(f'monitor_memory{{value="available"}} {mem.available}')
out.append(f'monitor_memory{{value="percent"}} {mem.percent}')
out.append(f'monitor_memory{{value="used"}} {mem.used}')
out.append(f'monitor_memory{{value="free"}} {mem.free}')
swap = psutil.swap_memory()
out.append(f'monitor_swap{{value="total"}} {swap.total}')
out.append(f'monitor_swap{{value="used"}} {swap.used}')
out.append(f'monitor_swap{{value="free"}} {swap.free}')
out.append(f'monitor_swap{{value="percent"}} {swap.percent}')
out.append(f'monitor_swap{{value="sin"}} {swap.sin}')
out.append(f'monitor_swap{{value="sout"}} {swap.sout}')
bootTime = psutil.boot_time()
out.append(f'monitor_boot_time{{}} {int(time.time() - bootTime)}')
netioConnections = psutil.net_connections()
out.append(f'monitor_netio_connections{{}} {len(netioConnections)}')
netioCounters = psutil.net_io_counters(nowrap=True)
out.append(f'monitor_netio_counters{{value="bytes_sent"}} {netioCounters.bytes_sent}')
out.append(f'monitor_netio_counters{{value="bytes_recv"}} {netioCounters.bytes_recv}')
out.append(f'monitor_netio_counters{{value="packets_sent"}} {netioCounters.packets_sent}')
out.append(f'monitor_netio_counters{{value="packets_recv"}} {netioCounters.packets_recv}')
out.append(f'monitor_netio_counters{{value="errin"}} {netioCounters.errin}')
out.append(f'monitor_netio_counters{{value="errout"}} {netioCounters.errout}')
out.append(f'monitor_netio_counters{{value="dropin"}} {netioCounters.dropin}')
out.append(f'monitor_netio_counters{{value="dropout"}} {netioCounters.dropout}')
return res.text("\n".join(out), status=200)
if __name__ == '__main__':
ip = "127.0.0.1"
port = 2611
if(len(sys.argv) > 1):
ip = sys.argv[1]
if(len(sys.argv) > 2):
try:
port = int(sys.argv[2])
except:
print("Port must be a numeric value!")
exit(1)
if(port < 1 or port > 0xffff):
print("Port must be in [1, 65535] range!")
exit(1)
app.run(host=ip, port=port, dev=DEV_MODE, access_log=ACCESS_LOG)

32
stuff/testEmail.py Normal file
View File

@ -0,0 +1,32 @@
import smtplib
import ssl
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from secrets import *
SMTP_HOST = 'smtp.office365.com'
SMTP_USER = 'webservice@furizon.net'
SMTP_PORT = 587
plain_body = "Test aaaa"
plain_text = MIMEText(plain_body, "plain")
message = MIMEMultipart("alternative")
message.attach(plain_text)
message['Subject'] = '[Furizon] This is a test!'
message['From'] = 'Furizon <webservice@furizon.net>'
message['To'] = f"Luca Sorace <strdjn@gmail.com>"
print("Start")
context = ssl.create_default_context()
print("Context created")
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as sender:
print("Created sender obj")
sender.starttls(context=context)
print("Tls started")
sender.login(SMTP_USER, SMTP_PASSWORD)
print("Logged in")
print(sender.sendmail(message['From'], message['to'], message.as_string()))
print("Mail sent")

31
tpl/admin.html Normal file
View File

@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}Admin panel{% endblock %}
{% block main %}
<main class="container">
<script src="/res/scripts/adminManager.js"></script>
<header>
<picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
</picture>
</header>
<!-- Quick controls -->
<h2>Admin panel</h2>
<p>Data</p>
<a href="/manage/admin/cache/clear" role="button" title="Reload the orders' data and the re-sync items' indexes from pretix">Clear cache</a>
<a download href="/manage/admin/export/export" role="button" title="Will export most of informations about the orders">Export CSV</a>
<a download href="/manage/admin/export/hotel" role="button" title="Will export a CSV for the hotel accomodation">Export hotel CSV</a>
<hr>
<p>Rooms</p>
<a href="/manage/nosecount" role="button" title="Shortcut to the nosecount's admin data">Manage rooms</a>
<a href="/manage/admin/room/verify" role="button" title="Will unconfirm rooms that fail the default check. Useful when editing answers from Pretix">Verify Rooms</a>
<a href="/manage/admin/room/wizard" role="button" title="Auto fill unconfirmed rooms. You can review and edit matches before confirming.">Fill Rooms</a>
<hr>
<p>Profiles</p>
<a href="#" onclick="confirmAction('propicReminder', this)" role="button" title="Will email all people who haven't uploaded a badge, yet" action="/manage/admin/propic/remind">Remind badge upload</a>
<a href="/manage/admin/room/autoconfirm" role="button" title="Will confirm all the full rooms that are still unconfirmed">Auto-confirm Rooms</a>
<hr>
{% include 'components/confirm_action_modal.html' %}
</main>
{% endblock %}

View File

@ -1,21 +1,15 @@
<!doctype html>
<html>
<head>
{% block head %}{% endblock %}
<meta charset="utf-8" />
<title>{% block title %}{% endblock %}</title>
<meta name="viewport" content="width=400rem" />
<meta name="supported-color-schemes" content="light dark">
<link rel="stylesheet" href="/res/pico.min.css">
<link rel="stylesheet" href="/res/styles/base.css">
<link rel="icon" type="image/x-icon" href="/res/icons/favicon.ico">
<style>
.propic-container {max-width:6em;margin:0 auto;}
.propic-container img {display:block;}
.propic {width:100%;border-radius:0.4em;margin:0 auto;border:4px solid #546e7a;}
.propic-flag {max-width:2em;margin-top:-1em;margin-left:auto;transform:translateX(0.7em);margin-right:0em;border-radius:2px;}
.propic-super {border:4px solid #fb8c00;}
.propic-normal {border:4px solid #8e24aa;}
.people div {text-align:center;}
.people h3, .people p, .people h5 {margin:0;}
.people {grid-auto-flow:row;}
@ -23,14 +17,12 @@
main {min-height: 30em;}
mark {background:#0f0;}
h1 img {max-height:1.4em;}
.notice {padding:0.8em;border-radius:3px;background:#e53935;color:#eee;}
.notice a {color:#eee;text-decoration:underline;}
.container {max-width:40em;padding:1em;box-sizing:border-box;}
.container {max-width:50em;padding:1em;box-sizing:border-box;}
td > a[role=button] {padding: 0.3em 0.7em;}
td {padding-left: 0.2em;padding-right: 0.2em;}
a[role=button] {margin: 0.25em 0;}
td > input[type=file] {margin:0;padding:0;}
section {margin-bottom:3em;}
@media (min-width: 500px) {body .grid {grid-template-columns: repeat(auto-fit, minmax(0%, 1fr));}}
@ -39,15 +31,8 @@
summary[role=button] {background:var(--primary-focus);color:var(--contrast);}
summary img, td img {height:1.2em;width:2em;box-sizing: border-box;}
@media only screen and (prefers-color-scheme: dark) {
.icon {filter: invert(1);}
}
nav {justify-content: normal;padding:0 0.5em;background:var(--card-background-color);}
nav a {display:inline-block;padding:0 0.6em;line-height:2em;margin:0;white-space:nowrap}
nav img {height:1.2em;display:inline;vertical-align:middle;box-sizing: border-box;}
nav {display:block;}
body .grid.people { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)) !important }
.status {float:right;}
@ -57,41 +42,16 @@
footer img {height:1.3em;}
.tag {background:var(--primary);color:var(--contrast);font-size:0.8em;font-weight:600;padding:0.1em 0.3em;border-radius:3px;}
.grid_2x2 {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
}
</style>
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setCampaignNameKey", "private_area"]);
{% if order %}
_paq.push(['setUserId', '{{order.code}}']);
{% endif %}
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://y.foxo.me/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '2']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="https://y.foxo.me/matomo.php?idsite=2&amp;rec=1" style="border:0;" alt="" /></p></noscript>
<!-- End Matomo Code -->
<script src="/res/scripts/base.js" defer></script>
</head>
<body>
<nav id="topbar">
{% if order %}
<a href="/manage/welcome">Your Booking</a>
{% endif %}
<a href="/manage/nosecount">Nose Count</a>
{% if order %}
<a href="/manage/carpooling">Carpooling</a>
<a style="float:right;" href="/manage/logout">Logout</a>
{% endif %}
<br clear="both" />
</nav>
<body onscroll="onScrollNav()">
{% include 'blocks/navbar.html' %}
{% block main %}{% endblock %}
<script type="text/javascript">
@ -106,7 +66,7 @@
});
</script>
<footer>
Made in 🇮🇹 by <img src="/res/icons/silhouette.svg" class="icon" /> · <a href="/manage/privacy">Privacy</a>
Made in 🇮🇹 by <a href="https://lab.foxo.me"><img src="/res/icons/silhouette.svg" class="icon" /></a> and maintained by <a href="https://twitter.com/stranckV2">Stranck</a> and <a href="https://about.woffo.ovh/">DrewThaWoof</a><br><a href="/manage/privacy">Privacy</a>
</footer>
</body>
</html>

View File

@ -1,35 +1,33 @@
<details id="badge">
<summary role="button"><img src="/res/icons/badge.svg" class="icon"/>Badge Customization {% if not order.ans('propic') %}<span class="status">⚠️</span>{% endif %}</summary>
<summary role="button"><img src="/res/icons/badge.svg" class="icon"/>Badge Customization {% if not order.isBadgeValid() %}<span class="status">⚠️</span>{% endif %}</summary>
{# Badge is always shown #}
<h2>Badge</h2>
{% if order.propic_locked %}
<p class="notice">⚠️ You have been limited from further editing your profile pic.</p>
{% endif %}
{% if (not order.ans('propic')) or (order.ans('is_fursuiter') != 'No' and not order.ans('propic_fursuiter')) %}
{% if not order.ans('propic') or (order.is_fursuiter and not order.ans('propic_fursuiter')) %}
<p class="notice">⚠️ One or more badge pictures are missing! This will cause you badge to be empty, so make sure to upload something before the deadline!</p>
{% endif %}
<form method="POST" enctype="multipart/form-data" action="/manage/propic/upload">
<div class="grid" style="text-align:center;margin-bottom:1em;">
<div>
{% with current=order, order=order, imgSrc='/res/propic/' + (order.ans('propic') or 'default.png'), effects = false %}
{% include 'blocks/propic.html' %}
{% endwith %}
<p>Normal Badge</p>
{% if not order.ans('propic') %}
<input type="file" value="" accept="image/jpeg,image/png" name="propic" />
{% else %}
<div class="propic-container">
<img src="/res/propic/{{order.ans('propic') or 'default.png'}}" class="propic" />
</div>
{% endif %}
<p>Normal Badge</p>
</div>
{% if order.ans('is_fursuiter') != 'No' %}
{% if order.is_fursuiter %}
<div>
{% with current=order, order=order, imgSrc='/res/propic/' + (order.ans('propic_fursuiter') or 'default.png'), effects = false %}
{% include 'blocks/propic.html' %}
{% endwith %}
<p>Fursuit Badge</p>
{% if not order.ans('propic_fursuiter') %}
<input type="file" value="" accept="image/jpeg,image/png" name="propic_fursuiter" />
{% else %}
<div class="propic-container">
<img src="/res/propic/{{order.ans('propic_fursuiter') or 'default.png'}}" class="propic" />
</div>
{% endif %}
<p>Fursuit Badge</p>
</div>
{% endif %}
</div>
@ -38,21 +36,16 @@
<p class="notice">⚠️ The deadline to upload pictures for the badge has expired. For last-minute changes, please contact the support over at <a href="mailto:info@furizon.net">info@furizon.net</a>. If your badge has been printed already, changing it will incur in a 2€ fee. You can also get extra badges at the reception for the same price. If you upload a propic now, it might not be printed on time.</p>
{% else %}
<p><em>
Min size: 64x64 - Max Size: 5MB, 2048x2048 - Formats: jpg, png<br />
Min size: {{PROPIC_MIN_SIZE[0]}}x{{PROPIC_MIN_SIZE[1]}} - Max Size: {{PROPIC_MAX_FILE_SIZE}}, {{PROPIC_MAX_SIZE[0]}}x{{PROPIC_MAX_SIZE[1]}} - Formats: jpg, png<br />
Photos whose aspect ratio is not a square will be cropped
Badge photos must clearly show the fursona/fursuit head.<br />Memes and low quality images will be removed and may limit your ability to upload pics in the future.
</em></p>
{% endif %}
<div class="grid">
{% if order.ans('propic') %}
<input type="submit" name="submit" value="Delete main image" {{'disabled' if time() > PROPIC_DEADLINE else ''}} />
{% endif %}
{% if order.ans('propic_fursuiter') %}
<input type="submit" name="submit" value="Delete fursuit image" {{'disabled' if time() > PROPIC_DEADLINE else ''}} />
{% endif %}
{% if (not order.ans('propic')) or (order.ans('is_fursuiter') != 'No' and not order.ans('propic_fursuiter')) %}
<input type="submit" name="submit" value="Upload" />
{% endif %}
<div class="grid grid_2x2">
<input style="grid-area: 1 / 1 / 2 / 3;" type="submit" name="submit" value="Upload" {{'disabled' if (order.ans('propic') and order.ans('propic_fursuiter')) else ''}} />
<input style="grid-area: 2 / 1 / 3 / 2;" type="submit" name="submit" value="Delete main image" {{'disabled' if (time() > PROPIC_DEADLINE or not order.ans('propic')) else ''}} />
<input style="grid-area: 2 / 2 / 3 / 3;" type="submit" name="submit" value="Delete fursuit image" {{'disabled' if (time() > PROPIC_DEADLINE or not order.ans('propic_fursuiter')) else ''}} />
</div>
</form>
</details>

23
tpl/blocks/navbar.html Normal file
View File

@ -0,0 +1,23 @@
<nav id="topbar">
<a id="mobileMenu">
<img class="nav-hamburger icon" src="/res/icons/menu.svg"/>
</a>
<div class="navbar-container">
{% if order %}
<a href="/manage/welcome">
<img class="navbar-propic" src="{{'/res/propic/' + (order.ans('propic') or 'default.png')}}"></img>{{order.ans('fursona_name')}}'s Booking
</a>
{% if order.isAdmin() %}<a class="" href="/manage/admin">Admin panel</a>{% endif %}
{% endif %}
<a href="/manage/nosecount">Nose Count</a>
<a href="/manage/fursuitcount">Fursuit Count</a>
<a href="/manage/sponsorcount" class="rainbow-text">Sponsor Count</a>
{% if order %}
<a href="/manage/carpooling">Carpooling</a>
<a class="align-right" href="/manage/logout">Logout</a>
{% endif %}
<br clear="both" />
</div>
</nav>

View File

@ -18,7 +18,7 @@
</tr>
</table>
{% if order.status == 'paid' and order.room_confirmed %}
<p style="text-align:right;"><a href="/manage/download_ticket?name=BEYOND-{{order.code}}.pdf" role="button">Download ticket</a></p>
<p style="text-align:right;"><a href="/manage/download_ticket?name=OVERLORD-{{order.code}}.pdf" role="button">Download ticket</a></p>
{% endif %}
{% if order.status != 'paid' %}
<a href="{{order.url}}"><button>Payment area</button></a>

21
tpl/blocks/propic.html Normal file
View File

@ -0,0 +1,21 @@
<div class="propic-container" >
{% if effects and (not order.sponsorship == None) %}
<div aria-hidden="true" class="absolute propic-border {{'propic-animated-' + (order.sponsorship or 'base')}}">
<div aria-hidden="true" class="propic-border-animation"></div>
</div>
<div aria-hidden="true" class="absolute propic-border blurred {{'propic-animated-' + (order.sponsorship or 'base')}}">
<div aria-hidden="true" class="propic-border-animation"></div>
</div>
{% endif %}
{% if current and current.isAdmin () and (not current.code == order.code ) and not nologin %}
<a class="control-login-as" href="/manage/admin/loginas/{{order.code}}">
{% endif %}
<img alt="{{order.ans('fursona_name') if order.ans('fursona_name') else 'A user'}}'s profile picture" src="{{imgSrc}}" class="absolute propic {{(('propic-' + order.sponsorship) if not effects else '') if order.sponsorship else 'propic-base'}}"/>
<svg aria-hidden="true" alt="" class="propic-filler {{(('propic-' + order.sponsorship) if not effects else '') if order.sponsorship else 'propic-base'}}" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"></svg>
{% if current and current.isAdmin () %}
</a>
{% endif %}
{% if flag %}
<img alt="flag" class="absolute propic-flag" src="/res/flags/{{order.country.lower()}}.svg"/>
{% endif %}
</div>

View File

@ -1,12 +1,18 @@
<details id="room">
<summary role="button"><img src="/res/icons/bedroom.svg" class="icon"/> Accomodation & Roommates <span class="status">{% if not order.room_confirmed %}⚠️{% endif %}</span></summary>
<h2>Your room {% if room_members %}- {{room_members[0].ans('room_name')}}{% endif %}</h2>
<summary role="button"><img src="/res/icons/bedroom.svg" class="icon"/> Accomodation & Roommates {% if not order.room_confirmed %}<span class="status">⚠️</span>{% endif %}</summary>
<h2 style="margin-bottom:0;">Your room {% if room_members %}- {{room_members[0].ans('room_name')}}{% endif %}</h2>
<p><b>Room's type:</b> {{ROOM_TYPE_NAMES[order.bed_in_room]}}.</p>
<p class="notice" style="background:#0881c0"><b>Note! </b> Only people with the same room type can be roommates. If you need help, contact the <a href="https://furizon.net/contact/">Furizon's Staff</a>.</p>
{% if not order.room_confirmed %}
<p class="notice" style="background:#0881c0"><b><a href="/manage/nosecount?filter=capacity">Check here</a> for any fur who share your room type.</p>
{% endif %}
{# Show alert if room owner has wrong people inside #}
{% if room_members and quota.get_left(len(room_members)) == 0 and (not order.room_confirmed) %}
<p class="notice">⚠️ Your room contains {{len(room_members)}} people inside, but sadly there are no more {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} rooms. You need to add or remove people until you reach the size of an available room if you want to confirm it.</p>
{% endif %}
{# {% if room_members and quota.get_left(len(room_members)) == 0 and (not order.room_confirmed) %} #}
{# <p class="notice">⚠️ Your room contains {{len(room_members)}} people inside, but sadly there are no more {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} rooms. You need to add or remove people until you reach the size of an available room if you want to confirm it.</p> #}
{# {% endif %} #}
{# Show alert if room was not confirmed #}
{% if order.room_id and not order.room_confirmed %}
@ -15,7 +21,8 @@
{# Show notice if the room is confirmed #}
{% if order.room_confirmed %}
<p class="notice" style="background:#060">✅ Your <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room has been confirmed</p>
{# <p class="notice" style="background:#060">✅ Your <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room has been confirmed</p> #}
<p class="notice" style="background:#060">✅ Your <strong>{{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}}</strong> room has been confirmed</p>
{% endif %}
{# Show roommates if room is set #}
@ -24,13 +31,12 @@
<div class="grid people" style="padding-bottom:1em;">
{% for person in room_members %}
<div style="margin-bottom: 1em;">
<div class="propic-container">
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
<img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
</div>
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h3>{{person.ans('fursona_name')}}</h3>
{% if person.code == order.room_id %}<p><strong style="color:#c6f">ROOM OWNER</strong></p>{% endif %}
<p>{{person.ans('staff_title') if person.ans('staff_title') else ''}} {{'Fursuiter' if person.ans('is_fursuiter') != 'No'}}</p>
<p>{{person.ans('staff_title') if person.ans('staff_title') else ''}} {{'Fursuiter' if person.is_fursuiter}}</p>
{% if person.status == 'pending' %}
<p><strong style="color:red;">UNPAID</strong></p>
{% endif %}
@ -42,7 +48,8 @@
{% endif %}
{% endfor %}
{% if order.room_id == order.code and not order.room_confirmed and len(room_members) < 5 %}
{# {% if order.room_id == order.code and not order.room_confirmed and len(room_members) < 5%} #}
{% if order.room_id == order.code and not order.room_confirmed and len(room_members) < order.room_person_no %}
<div>
<a href="javascript:document.getElementById('modal-roominvite').setAttribute('open', 'true');">
<div class="propic-container">
@ -72,14 +79,17 @@
{% endif %}
{% endif %}
<p class="grid">
<p class="grid grid_2x2">
{% if order.room_owner %}
{% if len(room_members) == 1 and not order.room_confirmed %}
<a href="/manage/room/delete" role="button">Delete room</a>
{% endif %}
{% if not order.room_confirmed %}
<a role="button" {% if not room.forbidden and quota.get_left(len(room_members)) > 0 %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room</a>
{# <a role="button" {% if not room.forbidden and quota.get_left(len(room_members)) > 0 %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room</a> #}
<a style="grid-area: 1 / 1 / 2 / 2;" role="button" href="javascript:document.getElementById('modal-roomrename').setAttribute('open', 'true');">Rename room</a>
<a style="grid-area: 1 / 2 / 2 / 3;" href="/manage/room/delete" role="button" {{'disabled' if (len(room_members) > 1) else ''}} >Delete room</a>
<a style="grid-area: 2 / 1 / 3 / 3; display:block;" role="button" {% if not room.forbidden and len(room_members) == order.room_person_no %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}}</strong> room</a>
{% else %}
{# <a style="grid-area: 1 / 1 / 2 / 2;" role="button" href="javascript:navigator.share({title: 'Furizon room', text:'Viewing room {{order.room_name}}', url: `${window.location.protocol}//${window.location.host}/manage/room/view/{{order.code}}}`});">Share</a> #}
{% endif %}
{% else %}
{% if order.room_id and not order.room_confirmed %}
@ -100,8 +110,10 @@
{% if person.status == 'pending' %}
<td><strong style="color:red;">UNPAID</strong></td>
{% endif %}
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/approve/{{person.code}}">Approve</a></td>
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/reject/{{person.code}}">Reject</a></td>
{% if order.room_owner %}
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/approve/{{person.code}}">Approve</a></td>
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/reject/{{person.code}}">Reject</a></td>
{% endif %}
</tr>
</div>
@ -113,13 +125,13 @@
{% endif %}
{# Room availability is always shown #}
<h4>Room availability</h4>
<table>
{% for q in quota.data['results'] if 'Room' in q['name'] %}
<tr {% if q['available_number'] == 0 %}style="text-decoration:line-through;"{% endif %}>
<td>{{q['name']}}</td>
<td>{{q['available_number']}} left</td>
</tr>
{% endfor %}
</table>
{# <h4>Room availability</h4> #}
{# <table> #}
{# {% for q in quota.data['results'] if 'Room' in q['name'] %} #}
{# <tr {% if q['available_number'] == 0 %}style="text-decoration:line-through;"{% endif %}> #}
{# <td>{{q['name']}}</td> #}
{# <td>{{q['available_number']}} left</td> #}
{# </tr> #}
{# {% endfor %} #}
{# </table> #}
</details>

View File

@ -29,21 +29,36 @@
<table>
<tr>
<td>Room type</td>
<td><strong>{{[None,'Single','Double','Triple','Quadruple','Quintuple'][len(room_members)]}} Room</strong></td>
</tr>
<tr>
<td>Rooms left of this type</td>
<td><strong>{{quota.get_left(len(room_members))}}</strong></td>
{# <td><strong>{{[None,'Single','Double','Triple','Quadruple','Quintuple'][len(room_members)]}} Room</strong></td> #}
<td><strong>{{[None,'Single','Double','Triple','Quadruple','Quintuple'][order.room_person_no]}} Room</strong></td>
</tr>
{# <tr> #}
{# <td>Rooms left of this type</td> #}
{# <td><strong>{{quota.get_left(len(room_members))}}</strong></td> #}
{# </tr> #}
</table>
<footer>
<a href="javascript:document.getElementById('modal-roomconfirm').removeAttribute('open')" role="button">Close</a>
<a href="/manage/room/confirm" role="button">Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room</a>
{# <a href="/manage/room/confirm" role="button">Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room</a> #}
<a href="/manage/room/confirm" role="button">Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}}</strong> room</a>
</footer>
</article>
</dialog>
<dialog id="modal-roomrename">
<article>
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
<h3>Rename this room</h3>
<p>Enter your room's new name!</p>
<p>This name will be public and shown in the room list, so nothing offensive! :)</p>
<form method="POST" action="/manage/room/rename">
<label for="name"></label>
<input type="text" name="name" required minlength="4" maxlength="64" value="{{order.ans('room_name')}}"/>
<input type="submit" value="Rename room" />
</form>
</article>
</dialog>
{% endif %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Furizon 2023 Carpooling{% endblock %}
{% block title %}Furizon 2024 Carpooling{% endblock %}
{% block main %}
<main class="container">
<header>
@ -42,13 +42,13 @@
Day of departure
<select name="day_departure" id="day_departure">
<option value="m29th" {{'selected' if order.carpooling_message.day_departure == 'm29th'}}>May 29th</option>
<option value="m30th" {{'selected' if order.carpooling_message.day_departure == 'm30th'}}>May 30th</option>
<option value="m31st" {{'selected' if order.carpooling_message.day_departure == 'm31st'}}>May 31st</option>
<option value="j1st" {{'selected' if order.carpooling_message.day_departure == 'j1st'}}>June 1st</option>
<option value="j2nd" {{'selected' if order.carpooling_message.day_departure == 'j2nd'}}>June 2nd</option>
<option value="j3rd" {{'selected' if order.carpooling_message.day_departure == 'j3rd'}}>June 3rd</option>
<option value="j4th" {{'selected' if order.carpooling_message.day_departure == 'j4th'}}>June 4th</option>
<option value="j5th" {{'selected' if order.carpooling_message.day_departure == 'j5th'}}>June 5th</option>
<option value="j6th" {{'selected' if order.carpooling_message.day_departure == 'j6th'}}>June 6th</option>
<option value="j7th" {{'selected' if order.carpooling_message.day_departure == 'j7th'}}>June 7th</option>
<option value="j8th" {{'selected' if order.carpooling_message.day_departure == 'j8th'}}>June 8th</option>
<option value="j9th" {{'selected' if order.carpooling_message.day_departure == 'j9th'}}>June 9th</option>
</select>
</label>
<textarea id="message" name="message" style="height:10em;" placeholder="Write here your message" required>{{order.carpooling_message.message}}</textarea>

View File

@ -0,0 +1,13 @@
<form id="intentFormAction" method="GET" action="">
<dialog id="modalRoomconfirm">
<article>
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
<h3 id="intentText">Confirm action</h3>
<p id="intentDescription"></p>
<div id="intentEditPanel"></div>
<footer>
<input id="intentSend" type="submit" value="Confirm" />
</footer>
</article>
</dialog>
</form>

View File

@ -0,0 +1,26 @@
<!--By Drew tha woof-->
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="supported-color-schemes" content="light dark">
<style media="all" type="text/css">
* { color: #bbc6ce; }
.body { width: 100%; margin: 0; background-color: #11191f; }
.container { max-width: 40em; padding: 1em; margin: 0 auto; }
.title { font-size: 1.75em; margin-bottom: 1.2em; color: #e1e6eb; margin-top: 0; font-family: sans-serif; }
.main-content { margin-top: 0; font-style: normal; font-weight: 400; font-family: sans-serif;}
.con-logo { height:3em;}
.link { text-decoration: none; background-color: #1095c1; color: #fff; padding: 1em; border-radius: 5px; font-weight: 600; }
</style>
</head>
<div class="body">
<div class="container">
<img src="https://reg.furizon.net/res/furizon.png" alt="con_logo" title="con_logo" class="con-logo">
<div>
<h2 class="title">{{title}}</h2>
<p class="main-content">{{body}}</p>
</div>
</div>
</div>
</html>

View File

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% block title %}Error {{exception.status_code}}{% endblock %}
{% block title %}Error {{status_code}}{% endblock %}
{% block main %}
<main class="container">
<h1>{{exception.status_code}}</h1>
<h1>{{status_code}}</h1>
<p>{{exception}}</p>
{% if exception.status_code == 409 %}
{% if status_code == 409 %}
<p>Retrying in 1 second...</p>
<meta http-equiv="refresh" content="1">
{% endif %}

27
tpl/fursuitcount.html Normal file
View File

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}Furizon 2024 Fursuitcount{% endblock %}
{% block main %}
<main class="container">
<header>
<picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
</picture>
</header>
<!--h2>Fursuit-count!</h2-->
<p>Welcome to the fursuit-count page! Here you can see all of the fursuits that you'll find at Furizon!</p>
{% for person in orders.values() if person.is_fursuiter%}
{% if loop.first %}
<div class="grid people" style="padding-bottom:1em;">
{% endif %}
<div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic_fursuiter') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% if loop.last %}</div>{% endif %}
{% endfor %}
</main>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Furizon 2023 Karaoke Admin{% endblock %}
{% block title %}Furizon 2024 Karaoke Admin{% endblock %}
{% block main %}
<main class="container">
<h1>Karaoke Admin</h1>

View File

@ -1,50 +1,99 @@
{% extends "base.html" %}
{% block title %}Furizon 2023 Nosecount{% endblock %}
{% block title %}Furizon 2024 Nosecount{% endblock %}
{% block head %}
<meta property="og:title" content="Nose count - Furizon" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:alt" content="Furizon logo" />
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
<meta property="og:description" content="Explore this year's rooms, find your friends and plan your meet-ups."/>
{% endblock %}
{% block main %}
<main class="container">
{% if order and order.isAdmin() %}
<script src="/res/scripts/roomManager.js"></script>
{% endif %}
<header>
<picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
</picture>
</header>
<p>Welcome to the nosecount page! Here you can see all of the available rooms at the convention, as well as the occupants currently staying in each room. Use this page to find your friends and plan your meet-ups.</p>
{% for o in orders.values() %}
{% if o.code == o.room_id and o.room_confirmed %}
<h4 style="margin-top:1em;">{{o.room_name}}</h4>
{% if filtered and order %}
{% for person in filtered.values() %}
{% if loop.first %}
<hr />
<p>{{filter_header}}</p>
<div class="grid people" style="padding-bottom:1em;">
{% endif %}
<div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% if loop.last %}</div>{% endif %}
{% endfor %}
{% endif %}
{% for o in orders.values() if (o.code == o.room_id and o.room_confirmed) %}
{% if loop.first %}
<hr />
<h1>Confirmed rooms{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
{% endif %}
<h4 style="margin-top:1em;">
<span>{{o.room_name}}</span>
{% if order and order.isAdmin() %}
<div class="room-actions">
<a onclick="confirmAction('rename', this)" action="/manage/admin/room/rename/{{o.code}}"><img src="/res/icons/pencil.svg" class="icon" /><span>Rename</span></a>
<a onclick="confirmAction('unconfirm', this)" action="/manage/admin/room/unconfirm/{{o.code}}"><img src="/res/icons/door_open.svg" class="icon" /><span>Unconfirm</span></a>
<a class="act-del" onclick="confirmAction('delete', this)" action="/manage/admin/room/delete/{{o.code}}"><img src="/res/icons/delete.svg" class="icon" /><span>Delete</span></a>
</div>
{% endif %}
</h4>
<div class="grid people" style="padding-bottom:1em;">
{% for m in o.room_members %}
{% if m in orders %}
{% with person = orders[m] %}
<div style="margin-bottom: 1em;">
<div class="propic-container">
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
<img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
</div>
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% endwith %}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endfor %}
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed and len(o.room_members) > 1) %}
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed) %}
{% if loop.first %}
<hr />
<h1>Unconfirmed rooms</h1>
<h1>Unconfirmed rooms{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</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>
{% endif %}
<h3>{{o.room_name}}</h3>
<h4>
<span>{{o.room_name}}</span>
{% if o.room_person_no - len(o.room_members) > 0 %} <span class="nsc-room-counter"> - Remaining slots: {{o.room_person_no - len(o.room_members)}}</span> {% endif %}
{% if order and order.isAdmin() %}
<div class="room-actions">
<a onclick="confirmAction('rename', this)" action="/manage/admin/room/rename/{{o.code}}"><img src="/res/icons/pencil.svg" class="icon" /><span>Rename</span></a>
<a class="act-del" onclick="confirmAction('delete', this)" action="/manage/admin/room/delete/{{o.code}}"><img src="/res/icons/delete.svg" class="icon" /><span>Delete</span></a>
</div>
{% endif %}
</h4>
<div class="grid people" style="padding-bottom:1em;">
{% for m in o.room_members %}
{% if m in orders %}
{% with person = orders[m] %}
<div style="margin-bottom: 1em;">
<div class="propic-container">
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
<img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
</div>
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% endwith %}
@ -54,22 +103,54 @@
{% if loop.last %}</div>{% endif %}
{% endfor %}
{% for person in orders.values() if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed)%}
{% for person in orders.values() if not person.room_id and (not person.room_confirmed) and not person.daily %}
{% if loop.first %}
<hr />
<h1>Roomless furs</h1>
<h1>Roomless furs{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</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>
<div class="grid people" style="padding-bottom:1em;">
{% endif %}
<div style="margin-bottom: 1em;">
<div class="propic-container">
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
<img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
</div>
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% if loop.last %}</div>{% endif %}
{% endfor %}
{% endfor %}
{% for person in orders.values() if person.daily %}
{% if loop.first %}
<hr />
<h1>Daily furs!{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
<p>These furs will not stay in our hotels, but may be there with us just a few days!</p>
<div class="grid people" style="padding-bottom:1em;">
{% endif %}
<div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% if loop.last %}</div>{% endif %}
{% endfor %}
<form id="intentFormAction" method="GET" action="">
<dialog id="modalOrderEditDialog">
<article>
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
<h3 id="intentText">Confirm room edit</h3>
<p id="intentDescription"></p>
<div id="intentEditPanel">
<label for="name">Enter a new room name</label>
<input id="intentRename" name="name" type="text" value="" maxlength="64"/>
</div>
<footer>
<input id="intentSend" type="submit" value="Confirm" />
</footer>
</article>
</dialog>
</form>
</main>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block title %}Furizon 2023 Nosecount{% endblock %}
{% block title %}Furizon 2024 Nosecount{% endblock %}
{% block main %}
<main class="container">
<header>
@ -8,7 +8,7 @@
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
</picture>
</header>
<p class="notice" style="margin:2em 0;">⚠️ This privacy policy is a courtesy explanation of <a href="https://furizon.net/wp-content/uploads/2023/01/Regolamento-Furizon-Beyond-en-GB.pdf">the one you signed up when registering to the con</a></p>
{# <p class="notice" style="margin:2em 0;">⚠️ This privacy policy is a courtesy explanation of <a href="https://furizon.net/wp-content/uploads/2023/01/Regolamento-Furizon-Beyond-en-GB.pdf">the one you signed up when registering to the con</a></p> #}
<h1>Privacy policy of this private area</h1>
<p>We collect only the data that is needed by law and to make sure that your convention experience is up to your expectations. Keep reading to know why and how we collect this data.</p>
@ -28,7 +28,6 @@
<li>First three octets of your ip address</li>
<li>List of status codes (404, 500, 403...) you encountered, and when you encountered them</li>
</ul>
<p>This info is collected through our first-party domain <em>y.foxo.me</em></p>
<h2>Backups and export of data</h2>
<p>All data is stored in servers managed by Furizon APS, inside of facilities in the Italian territory. No data is exported outside of the italian territory. All communication uses end to end encryption.</p>

52
tpl/sponsorcount.html Normal file
View File

@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}Furizon 2024 Sponsorcount{% endblock %}
{% block head %}
<meta property="og:title" content="Sponsor count - Furizon" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:alt" content="Furizon logo" />
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
<meta property="og:description" content="Thanks to all the amazing furs who decided to support us this year ❤️"/>
{% endblock %}
{% block main %}
<main class="container">
<header>
<picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
</picture>
</header>
<h2 class="rainbow-text">Sponsor count!</h2>
<p>Welcome to the sponsor-count page! This is the list of users that support us! Thanks really a lot to everyone ❤️</p>
{% for person in orders.values() if person.sponsorship == "super"%}
{% if loop.first %}
<hr />
<h1>Super sponsors!</h1>
<div class="grid people" style="padding-bottom:1em;">
{% endif %}
<div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% if loop.last %}</div>{% endif %}
{% endfor %}
{% for person in orders.values() if person.sponsorship == "normal"%}
{% if loop.first %}
<hr />
<h1>Sponsors</h1>
<div class="grid people" style="padding-bottom:1em;">
{% endif %}
<div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% if loop.last %}</div>{% endif %}
{% endfor %}
</main>
{% endblock %}

24
tpl/view_room.html Normal file
View File

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}{{room_data['name']}}{% endblock %}
{% block head %}
<!--Open Graph tags here-->
<meta property="og:title" content="View room - Furizon" />
<meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:alt" content="View of a room" />
<meta property="og:image" content="http://localhost:8188/{{preview}}" />
<meta property="og:image:secure_url" content="http://localhost:8188/{{preview}}" />
<meta property="og:image:width" content="{{230 * room_data['capacity'] + 130}}"/>
<meta property="og:image:height" content="270"/>
<meta property="og:description" content="Room {{room_data['name']}} has {{'been confirmed.' if room_data['confirmed'] else ('been filled.' if room_data['free_spots'] == 0 else (room_data['free_spots'] | string) + ' free spots out of ' + (room_data['capacity'] | string )) }}"/>
{% endblock %}
{% block main %}
<main class="container">
<header>
<picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;" onload="window.location.href = '/manage/nosecount/'">
</picture>
</header>
</main>
{% endblock %}

View File

@ -1,6 +1,16 @@
{% extends "base.html" %}
{% block title %}{{order.name}}'s Booking{% endblock %}
{% block head %}
<!--Open Graph tags here-->
<meta property="og:title" content="Furizon booking management" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:alt" content="Furizon's logo" />
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
{% endblock %}
{% block main %}
{% set locale = order.get_language() %}
<main class="container">
<header>
<picture>
@ -12,7 +22,7 @@
<p>From here, you can easily manage all aspects of your booking, including composing your hotel room, designing your badge, and updating your payment information. Simply use the buttons below to navigate between the different sections.</p>
<p>Buttons marked with ⚠️ require your attention</p>
<p>If you have any questions or issues while using this page, please don't hesitate to contact us for assistance. We look forward to seeing you at Furizon Riverside!</p>
<p>If you have any questions or issues while using this page, please don't hesitate to contact us for assistance. We look forward to seeing you at Furizon Overlord!</p>
<hr />
<h2 id="info">Useful information</h2>
<table>
@ -24,13 +34,14 @@
</tr>
<tr>
<th>When{{' (convention)' if order.has_early or order.has_late else ''}}?</th>
<td>13 October → 15 October 2023</td></td>
{# This should be early/late excluded! #}
<td><img src="/res/icons/calendar.svg" class="icon" />4 June → 8 June 2024</td></td>
</tr>
{% if order.has_early or order.has_late %}
<tr>
<th>When (check-in)?</th>
<td>
{{('12' if order.has_early else '13')|safe}} October → {{('16' if order.has_late else '15')|safe}} October 2023
{{('3' if order.has_early else '4')|safe}} June → {{('9' if order.has_late else '8')|safe}} June 2024
{% if order.has_early %}
<span class="tag">EARLY</span>
{% endif %}
@ -53,19 +64,36 @@
{% if order.status == 'paid' and order.room_confirmed %}
<br />
<img src="/res/icons/pdf.svg" class="icon" />
<a href="/manage/download_ticket?name=RIVERSIDE-{{order.code}}.pdf" target="_blank">Download ticket PDF</a>
<a href="/manage/download_ticket?name=OVERLORD-{{order.code}}.pdf" target="_blank">Download ticket PDF</a>
{% endif %}
</td>
</tr>
{% if order.shuttle_bus %}
<tr>
<th>Shuttle</th>
<td>
<img src="/res/icons/bus.svg" class="icon" />
{{order.shuttle_bus}}
</td>
</tr>
{% endif %}
</table>
<h2>Manage your booking</h2>
{% include 'blocks/payment.html' %}
{% if order.position_positiontypeid not in ITEM_IDS['daily'] %}
{% if not order.daily %}
{% include 'blocks/room.html' %}
{% endif %}
{% include 'blocks/badge.html' %}
<details id="shuttle">
<summary role="button"><img src="/res/icons/bus.svg" class="icon" />Shuttle</summary>
<p><b>Due to the low number of requests, the shuttle service managed by Trentino Trasporti will not be available. Those who have purchased a bus ticket will be refunded directly by the transport company</b></p>
<p>On the Furizon Telegram group, there is an active topic dedicated to car sharing, and the staff is available to look for custom alternative solutions. We apologize for the inconvenience.</p>
<!--p>This year, a shuttle service operated by the tourism company of Val di Fiemme will be available. The shuttle service will consist of a bus serving the convention, with scheduled stops at major airports and train stations. More informations <a href="https://furizon.net/furizon-overlord/furizon-overlord-shuttle-bus/">in the dedicated page.</a></p>
<p style="text-align:right;"><a href="{{LOCALES['shuttle_link_url'][locale]}}" target="_blank" role="button">Book now!</a></p-->
</details>
<details id="barcard">
<summary role="button"><img src="/res/icons/bar.svg" class="icon" />Barcard</summary>
<p>This year's badges will be NFC-enabled and serve as a digital barcard, allowing you to load 'drinks' onto your badge and use it to purchase beverages at the bar without the need for physical cash or the risk of losing a paper barcard. The barcard system will be enabled closer to the convention, so you will have the opportunity to load your badge in advance and enjoy a convenient, cashless experience at the event. Keep an eye out for updates on when the system will be live and available for use.</p>

88
tpl/wizard.html Normal file
View File

@ -0,0 +1,88 @@
{% extends "base.html" %}
{% block title %}Room Wizard{% endblock %}
{% block head %}
<link rel="stylesheet" href="/res/styles/wizard.css">
{% endblock %}
{% block main %}
<main class="container">
<script src="/res/scripts/wizardManager.js" type="text/javascript" defer="defer"></script>
<header>
<picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
</picture>
</header>
<!--order = current order login
unconfirmed_orders = all non confirmed rooms orders
all_orders = all orders
data = assigned rooms -->
<h2>Review rooms <a href="#popover-empty-room-tip" onclick="document.querySelector('#popover-wizard-tip').showPopover()">?</a></h2>
<div popover id="popover-wizard-tip">This is the preview page. Re-arrange users by dragging and dropping them in the rooms.<br>Once finished, scroll down to either <i>'Confirm'</i> changes or <i>'Undo'</i> them.</div>
<hr>
{% for room in data.items() %}
{% if room[0] in all_orders %}
{%with room_order = unconfirmed_orders[room[0]] %}
<div class="room" id="room-{{room_order.code}}" room-type="{{room_order.bed_in_room}}" room-size="{{room_order.room_person_no - len(room_order.room_members)}}" current-size="{{len(room[1]['to_add'])}}">
<h4 style="margin-top:1em;">
<span>{{room_order.room_name if room_order.room_name else room[1]['room_name'] if room[1] and room[1]['room_name'] else ''}}</span>
</h4>
<div class="grid people" style="padding-bottom:1em;">
{% for m in room_order.room_members %}
{% if m in all_orders %}
{% with person = all_orders[m] %}
<div class="edit-disabled" style="margin-bottom: 1em;">
{% with current=None, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% endwith %}
{% endif %}
{% endfor %}
{% for m in room[1]['to_add'] %}
{% if m in unconfirmed_orders %}
{% with person = unconfirmed_orders[m] %}
<div class="edit-drag" id="{{person.code}}" room-type="{{person.bed_in_room}}" style="margin-bottom: 1em;" draggable="true">
{% with current=None, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %}
{% include 'blocks/propic.html' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
</div>
{% endwith %}
{% endif %}
{% endfor %}
</div>
</div>
{% endwith %}
{% endif %}
{% endfor %}
<div class="room" infinite="true" id="room-infinite" room-size="999999999" current-size="0">
<h4 style="margin-top:1em;">Empty room <a href="#popover-empty-room-tip" onclick="document.querySelector('#popover-empty-room-tip').showPopover()">?</a></button></h4>
<div popover id="popover-empty-room-tip">This is a placeholder room. Place users temporarily in order to free space and arrange rooms</div>
<div class="grid people" style="padding-bottom:1em;"></div>
</div>
<a href="/manage/admin" role="button" title="Discard all changes and go back to the admin page">Undo</a>
<a href="#" class="align-right" onclick="onSave()" role="button" title="Will try saving current changes.">Confirm changes</a>
<dialog id="modalConfirmDialog">
<article>
<a href="#close" id="modalClose" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
<h3 id="intentText">Confirm arrangement?</h3>
<p id="intentDescription">
Roomless guests will be moved around existing rooms and newly generated ones.<br>
This will also confirm all rooms.
</p>
<div popover id="popover-status"><span id="popover-status-text"></span></div>
<footer>
<button id="intentSend" onclick="submitData(this)">Confirm</button>
</footer>
</article>
</dialog>
<script type="text/javascript">
let saveData = JSON.parse('{{jsondata|safe}}');
</script>
</main>
{% endblock %}

339
utils.py Normal file
View File

@ -0,0 +1,339 @@
from os.path import join
from sanic import exceptions
from config import *
import httpx
from messages import ROOM_ERROR_TYPES
from email_util import send_unconfirm_message
from sanic.response import text, html, redirect, raw
from sanic.log import logger
from metrics import *
import pretixClient
import traceback
METADATA_TAG = "meta_data"
VARIATIONS_TAG = "variations"
QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.html
"number": "N",
"one_line_string": "S",
"multi_line_string": "T",
"boolean": "B",
"choice_from_list": "C",
"multiple_choice_from_list": "M",
"file_upload": "F",
"date": "D",
"time": "H",
"date_time": "W",
"country_code": "CC",
"telephone_number": "TEL"
}
TYPE_OF_QUESTIONS = {} # maps questionId -> type
async def load_questions() -> bool:
global TYPE_OF_QUESTIONS
# TYPE_OF_QUESTIONS.clear() It should not be needed
logger.info("[QUESTIONS] Loading questions...")
success = True
try:
p = 0
while 1:
p += 1
res = await pretixClient.get(f"questions/?page={p}", expectedStatusCodes=[200, 404])
if res.status_code == 404: break
data = res.json()
for q in data['results']:
TYPE_OF_QUESTIONS[q['id']] = q['type']
except Exception:
logger.warning(f"[QUESTIONS] Error while loading questions.\n{traceback.format_exc()}")
success = False
return success
async def load_items() -> bool:
global ITEMS_ID_MAP
global ITEM_VARIATIONS_MAP
global CATEGORIES_LIST_MAP
global ROOM_TYPE_NAMES
logger.info("[ITEMS] Loading items...")
success = True
try:
p = 0
while 1:
p += 1
res = await pretixClient.get(f"items/?page={p}", expectedStatusCodes=[200, 404])
if res.status_code == 404: break
data = res.json()
for q in data['results']:
# Map item id
itemName = check_and_get_name ('item', q)
if itemName and itemName in ITEMS_ID_MAP:
ITEMS_ID_MAP[itemName] = q['id']
# If item has variations, map them, too
if itemName in ITEM_VARIATIONS_MAP and VARIATIONS_TAG in q:
isBedInRoom = itemName == 'bed_in_room'
for v in q[VARIATIONS_TAG]:
variationName = check_and_get_name('variation', v)
if variationName and variationName in ITEM_VARIATIONS_MAP[itemName]:
ITEM_VARIATIONS_MAP[itemName][variationName] = v['id']
if isBedInRoom and variationName in ITEM_VARIATIONS_MAP['bed_in_room']:
roomName = v['name'] if 'name' in v and isinstance(v['name'], str) else None
if not roomName and 'value' in v:
roomName = v['value'][list(v['value'].keys())[0]]
ROOM_TYPE_NAMES[v['id']] = roomName
# Adds itself to the category list
categoryName = check_and_get_category ('item', q)
if not categoryName or q['id'] in CATEGORIES_LIST_MAP[categoryName]: continue
CATEGORIES_LIST_MAP[categoryName].append(q['id'])
if (EXTRA_PRINTS):
logger.debug(f'Mapped Items: %s', ITEMS_ID_MAP)
logger.debug(f'Mapped Variations: %s', ITEM_VARIATIONS_MAP)
logger.debug(f'Mapped categories: %s', CATEGORIES_LIST_MAP)
logger.debug(f'Mapped Rooms: %s', ROOM_TYPE_NAMES)
except Exception:
logger.warning(f"[ITEMS] Error while loading items.\n{traceback.format_exc()}")
success = False
return success
# Tries to get an item name from metadata. Prints a warning if an item has no metadata
def check_and_get_name(type, q):
itemName = extract_metadata_name(q)
if not itemName and EXTRA_PRINTS:
logger.warning('%s %s has not been mapped.', type, q['id'])
return itemName
def check_and_get_category (type, q):
categoryName = extract_category (q)
if not categoryName and EXTRA_PRINTS:
logger.warning('%s %s has no category set.', type, q['id'])
return categoryName
# Checks if the item has specified metadata name
def internal_name_check (toExtract, name):
return toExtract and name and METADATA_TAG in toExtract and toExtract[METADATA_TAG][METADATA_NAME] == str(name)
# Returns the item_name metadata from the item or None if not defined
def extract_metadata_name (toExtract):
return extract_data(toExtract, [METADATA_TAG, METADATA_NAME])
# Returns the category_name metadata from the item or None if not defined
def extract_category (toExtract):
return extract_data(toExtract, [METADATA_TAG, METADATA_CATEGORY])
def extract_data (dataFrom, tags):
data = dataFrom
for t in tags:
if t not in data: return None
data = data[t]
return data
def key_from_value(dict, value):
return [k for k,v in dict.items() if v == value]
def sizeof_fmt(num, suffix="B"):
for unit in ("", "K", "M", "G", "T", "P", "E", "Z"):
if abs(num) < 1000.0:
return f"{num:3.1f}{unit}{suffix}"
num /= 1000.0
return f"{num:.1f}Yi{suffix}"
async def get_order_by_code(request, code, throwException=False):
res = await request.app.ctx.om.get_order(code=code)
if not throwException:
return res
if res is None:
raise exceptions.BadRequest(f"[getOrderByCode] Code {code} not found!")
return res
async def get_people_in_room_by_code(request, code, om=None):
if not om: om = request.app.ctx.om
await om.update_cache()
return list(filter(lambda rm: rm.room_id == code, om.cache.values()))
async def confirm_room_by_order(order, request):
bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room
room_members = []
for m in order.room_members:
if m == order.code:
res = order
else:
res = await request.app.ctx.om.get_order(code=m)
if res.room_id != order.code:
raise exceptions.BadRequest("Please contact support: some of the members in your room are actually somewhere else")
if res.status != 'paid':
raise exceptions.BadRequest("Somebody hasn't paid.")
if res.bed_in_room != bed_in_room:
raise exceptions.BadRequest("Somebody has a ticket for a different type of room!")
if res.daily:
raise exceptions.BadRequest("Somebody in your room has a daily ticket!")
room_members.append(res)
if len(room_members) != order.room_person_no:
raise exceptions.BadRequest("The number of people in your room mismatches your type of ticket!")
for rm in room_members:
await rm.edit_answer('room_id', order.code)
await rm.edit_answer('room_confirmed', "True")
await rm.edit_answer('pending_roommates', None)
await rm.edit_answer('pending_room', None)
for rm in room_members:
await rm.send_answers()
async def unconfirm_room_by_order(order, room_members=None, throw=True, request=None, om=None):
if not om: om = request.app.ctx.om
if not order.room_confirmed:
if throw:
raise exceptions.BadRequest("Room is not confirmed!")
else:
return
room_members = await get_people_in_room_by_code(request, order.code, om) if not room_members or len(room_members) == 0 else room_members
for p in room_members:
await p.edit_answer('room_confirmed', "False")
await p.send_answers()
async def remove_members_from_room(order, removeMembers):
didSomething = False
for member in removeMembers:
if (member in order.room_members):
order.room_members.remove(member)
didSomething = True
if(didSomething):
await order.edit_answer("room_members", ','.join(order.room_members))
await order.send_answers()
return didSomething
async def validate_rooms(request, rooms, om):
logger.info('Validating rooms...')
if not om: om = request.app.ctx.om
# rooms_to_unconfirm is the room that MUST be unconfirmed, room_with_errors is a less strict set containing all rooms with kind-ish errors
rooms_to_unconfirm = []
room_with_errors = []
remove_members = []
# Validate rooms
for order in rooms:
result = await check_room(request, order, om)
if(len(order.room_errors) > 0):
room_with_errors.append(result)
check = result[1]
if check != None and check == False:
rooms_to_unconfirm.append(result)
# End here if no room has failed check
if len(room_with_errors) == 0:
logger.info('[ROOM VALIDATION] Every room passed the check.')
return
roomErrListSrts = []
for fr in room_with_errors:
for error in fr[0].room_errors:
roomErrListSrts.append(f"[ROOM VALIDATION] [ERR] Parent room: {fr[0].code} {'C' if fr[0].room_confirmed else 'N'} | Order {error[0] if error[0] else '-----'} with code {error[1]}")
logger.warning(f'[ROOM VALIDATION] Room validation failed for orders: \n%s', "\n".join(roomErrListSrts))
# Get confirmed rooms that fail validation
failed_confirmed_rooms = list(filter(lambda fr: (fr[0].room_confirmed == True), rooms_to_unconfirm))
didSomething = False
if len(failed_confirmed_rooms) == 0:
logger.info('[ROOM VALIDATION] No rooms to unconfirm.')
else:
didSomething = True
logger.info(f"[ROOM VALIDATION] Trying to unconfirm {len(failed_confirmed_rooms)} rooms...")
# Try unconfirming them
for rtu in failed_confirmed_rooms:
order = rtu[0]
member_orders = rtu[2]
logger.warning(f"[ROOM VALIDATION] [UNCONFIRMING] Unconfirming room {order.code}...")
# Unconfirm and email users about the room
if UNCONFIRM_ROOMS_ENABLE:
await unconfirm_room_by_order(order, member_orders, False, None, om)
for r in rooms_to_unconfirm:
order = r[0]
removeMembers = r[3]
if len(removeMembers) > 0:
logger.warning(f"[ROOM VALIDATION] [REMOVING] Removing members '{','.join(removeMembers)}' from room {order.code}")
if UNCONFIRM_ROOMS_ENABLE:
didSomething |= await remove_members_from_room(order, removeMembers)
if(r not in failed_confirmed_rooms): failed_confirmed_rooms.append(r)
if(didSomething):
logger.info(f"[ROOM VALIDATION] Sending unconfirm notice to room members...")
sent_count = 0
# Send unconfirm notice via email
for rtu in failed_confirmed_rooms:
order = rtu[0]
member_orders = rtu[2]
try:
if UNCONFIRM_ROOMS_ENABLE:
await send_unconfirm_message(order, member_orders)
sent_count += len(member_orders)
except Exception as ex:
if EXTRA_PRINTS: logger.exception(str(ex))
logger.info(f"[ROOM VALIDATION] Sent {sent_count} emails")
async def check_room(request, order, om=None):
room_errors = []
room_members = []
remove_members = []
use_cached = request == None
if not om: om = request.app.ctx.om
if not order or not order.room_id or order.room_id != order.code: return (order, False, room_members, remove_members)
# This is not needed anymore you buy tickets already
#if quotas.get_left(len(order.room_members)) == 0:
# raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
allOk = True
bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room
for m in order.room_members:
if m == order.code:
res = order
else:
res = await om.get_order(code=m, cached=use_cached)
# Room user in another room
if res.room_id != order.code:
room_errors.append((res.code, 'room_id_mismatch'))
allOk = False
if res.status == 'canceled':
room_errors.append((res.code, 'canceled'))
remove_members.append(res.code)
allOk = False
elif res.status != 'paid':
room_errors.append((res.code, 'unpaid'))
if res.bed_in_room != bed_in_room:
room_errors.append((res.code, 'type_mismatch'))
if order.room_confirmed:
allOk = False
if res.daily:
room_errors.append((res.code, 'daily'))
if order.room_confirmed:
allOk = False
room_members.append(res)
if len(room_members) != order.room_person_no and order.room_person_no != None and order.room_person_no >= 0:
room_errors.append((None, 'capacity_mismatch'))
if order.room_confirmed:
allOk = False
order.set_room_errors(room_errors)
return (order, allOk, room_members, remove_members)