Compare commits

..

No commits in common. "stranck-dev" and "main" have entirely different histories.

86 changed files with 492 additions and 6905 deletions

9
.gitignore vendored
View File

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

View File

@ -1,26 +0,0 @@
# 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`.

384
admin.py
View File

@ -1,384 +0,0 @@
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, 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)
await clear_cache(request, order)
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: (x[1].room_person_no, len(x[1].room_members)), reverse=True) if (value.status not in ['canceled', 'expired'] and not value.daily and value.bed_in_room != ITEM_VARIATIONS_MAP["bed_in_room"]["bed_in_room_no_room"])}
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 and value.bed_in_room != ITEM_VARIATIONS_MAP["bed_in_room"]["bed_in_room_no_room"])}
# Result map
result_map = {}
# Check overflows
room_quota_overflow = {}
for key, value in ITEM_VARIATIONS_MAP['bed_in_room'].items():
if key != "bed_in_room_no_room":
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, all_orders.values())))
room_quota_overflow[value] = current_quota - int(room_quota.size / capacity) if room_quota else 0
if DEV_MODE and EXTRA_PRINTS:
print(f"There are {current_quota} of room type {key} out of a total of ({room_quota.size} / {capacity})")
# 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))
sorted_rooms = [r for r in sorted_rooms if r.bed_in_room == room_type]
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 = []
count = room.room_person_no
alreadyPresent = len(room.room_members)
missing_slots = count - alreadyPresent
for _ 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,
'count': count,
'previouslyPresent': alreadyPresent
}
generated_counter = 0
# Create additional rooms
while len(roomless_orders.items()) > 0:
room = list(roomless_orders.items())[0][1]
to_add = []
count = room.room_person_no
alreadyPresent = len(room.room_members)
missing_slots = count - alreadyPresent
for _ 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,
'count': count,
'previouslyPresent': alreadyPresent
}
result_map["infinite"] = { 'to_add': [] }
result_map = {k: v for k, v in sorted(result_map.items(), key=lambda x: ((x[1]["count"], x[1]["previouslyPresent"]) if("count" in x[1] and "previouslyPresent" in x[1]) else (4316, 0) ))}
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 clear_cache(request, order)
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.") # Since we don't confirm rooms anymore, we don't need to check if they're paid or not
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") Use the autoconfirm button in the admin panel
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 clear_cache(request, order)
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"})

34
api.py
View File

@ -9,9 +9,6 @@ import random
import string import string
import httpx import httpx
import json import json
import traceback
from sanic.log import logger
from email_util import send_app_login_attempt
bp = Blueprint("api", url_prefix="/manage/api") bp = Blueprint("api", url_prefix="/manage/api")
@ -34,8 +31,7 @@ async def api_members(request):
'propic_fursuiter': o.ans('propic_fursuiter'), 'propic_fursuiter': o.ans('propic_fursuiter'),
'staff_role': o.ans('staff_role'), 'staff_role': o.ans('staff_role'),
'country': o.country, 'country': o.country,
'room_id': o.room_id, 'is_checked_in': False,
'is_checked_in': o.checked_in,
'points': random.randint(0,50) if random.random() > 0.3 else 0 'points': random.randint(0,50) if random.random() > 0.3 else 0
}) })
@ -114,10 +110,6 @@ async def token_test(request):
return response.json({'ok': True, 'message': 'This token is valid :)'}) return response.json({'ok': True, 'message': 'This token is valid :)'})
@bp.get("/ping")
async def ping(request):
return response.text("pong")
@bp.get("/welcome") @bp.get("/welcome")
async def welcome_app(request): async def welcome_app(request):
@ -144,18 +136,15 @@ async def welcome_app(request):
'propic_fursuiter': o.ans('propic_fursuiter'), 'propic_fursuiter': o.ans('propic_fursuiter'),
'staff_role': o.ans('staff_role'), 'staff_role': o.ans('staff_role'),
'country': o.country, 'country': o.country,
'is_checked_in': o.checked_in, 'is_checked_in': False,
'points': random.randint(0,50) if random.random() > 0.3 else 0, 'points': random.randint(0,50) if random.random() > 0.3 else 0,
'can_scan_nfc': o.can_scan_nfc, 'can_scan_nfc': o.can_scan_nfc,
'room_id': o.room_id,
#'mail': o.email,
'actual_room_id': o.actual_room, 'actual_room_id': o.actual_room,
**ret **ret
}) })
@bp.get("/scan/<nfc_id>") @bp.get("/scan/<nfc_id>")
async def nfc_scan(request, nfc_id): async def nfc_scan(request, nfc_id):
return response.text("Nope")
if not request.token: if not request.token:
return response.json({'ok': False, 'error': 'You need to provide a token.'}, status=401) return response.json({'ok': False, 'error': 'You need to provide a token.'}, status=401)
@ -182,12 +171,11 @@ async def nfc_scan(request, nfc_id):
'propic_fursuiter': o.ans('propic_fursuiter'), 'propic_fursuiter': o.ans('propic_fursuiter'),
'staff_role': o.ans('staff_role'), 'staff_role': o.ans('staff_role'),
'country': o.country, 'country': o.country,
'is_checked_in': o.checked_in, 'is_checked_in': False,
'points': random.randint(0,50) if random.random() > 0.3 else 0, 'points': random.randint(0,50) if random.random() > 0.3 else 0,
'comment': o.comment, 'comment': o.comment,
'actual_room_id': o.actual_room, 'actual_room_id': o.actual_room,
'phone': o.phone, 'phone': o.phone,
'room_id': o.room_id,
'telegram_username': o.telegram_username, 'telegram_username': o.telegram_username,
'roommates': {x: (await request.app.ctx.om.get_order(code=x, cached=True)).name for x in room_owner.room_members if x != o.code} 'roommates': {x: (await request.app.ctx.om.get_order(code=x, cached=True)).name for x in room_owner.room_members if x != o.code}
}) })
@ -197,8 +185,7 @@ async def nfc_scan(request, nfc_id):
@bp.get("/get_token/<code>/<login_code>") @bp.get("/get_token/<code>/<login_code>")
async def get_token_from_code(request, code, login_code): async def get_token_from_code(request, code, login_code):
if not code in request.app.ctx.login_codes: if not code in request.app.ctx.login_codes:
if DEV_MODE and EXTRA_PRINTS: print(request.app.ctx.login_codes)
logger.debug(request.app.ctx.login_codes)
return response.json({'ok': False, 'error': 'You need to reauthenticate. The code has expired.'}, status=401) 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: if request.app.ctx.login_codes[code][1] == 0:
@ -232,9 +219,16 @@ async def get_token(request, code):
request.app.ctx.login_codes[code] = [''.join(random.choice(string.digits) for _ in range(6)), 3] request.app.ctx.login_codes[code] = [''.join(random.choice(string.digits) for _ in range(6)), 3]
try: try:
await send_app_login_attempt(user, request.app.ctx.login_codes[code][0]) 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!")
except Exception: msg['Subject'] = '[Furizon] Your login code'
logger.error(f"[API] [GET_TOKEN] Error while sending email.\n{traceback.format_exc()}") 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:
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': 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.'}) return response.json({'ok': True, 'message': 'A login code has been sent to your email.'})

146
app.py
View File

@ -1,26 +1,20 @@
from sanic import Sanic, response, exceptions from sanic import Sanic, response, exceptions
from sanic.response import text, html, redirect, raw from sanic.response import text, html, redirect, raw
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from time import time, sleep from time import time
import httpx import httpx
import re
import json
import logging
from os.path import join from os.path import join
from ext import * from ext import *
from config import * from config import *
from aztec_code_generator import AztecCode from aztec_code_generator import AztecCode
from propic import resetDefaultPropic
from io import BytesIO from io import BytesIO
from asyncio import Queue from asyncio import Queue
from messages import LOCALES
import sqlite3 import sqlite3
import requests
import sys
from sanic.log import logger, logging, access_logger
from metrics import *
from utils import isSessionAdmin
from email_util import killSmptClient
import pretixClient
import traceback
log = logging.getLogger()
app = Sanic(__name__) app = Sanic(__name__)
app.static("/res", "res/") app.static("/res", "res/")
@ -31,49 +25,33 @@ app.ext.add_dependency(Quotas, get_quotas)
from room import bp as room_bp from room import bp as room_bp
from propic import bp as propic_bp from propic import bp as propic_bp
from karaoke import bp as karaoke_bp from karaoke import bp as karaoke_bp
from export import bp as export_bp
from stats import bp as stats_bp from stats import bp as stats_bp
from api import bp as api_bp from api import bp as api_bp
from carpooling import bp as carpooling_bp from carpooling import bp as carpooling_bp
from checkin import bp as checkin_bp from checkin import bp as checkin_bp
from admin import bp as admin_bp
app.blueprint([room_bp, karaoke_bp, propic_bp, stats_bp, api_bp, carpooling_bp, checkin_bp, admin_bp]) app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp])
@app.exception(exceptions.SanicException)
async def clear_session(response): async def clear_session(request, exception):
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') tpl = app.ctx.tpl.get_template('error.html')
r = html(tpl.render(exception=exception, status_code=statusCode), status=statusCode) r = html(tpl.render(exception=exception))
except:
traceback.print_exc()
if statusCode == 403: if exception.status_code == 403:
await clear_session(r) del r.cookies["foxo_code"]
del r.cookies["foxo_secret"]
return r return r
@app.before_server_start @app.before_server_start
async def main_start(*_): async def main_start(*_):
logger.info(f"[{app.name}] >>>>>> main_start <<<<<<") print(">>>>>> main_start <<<<<<")
logger.setLevel(LOG_LEVEL)
access_logger.addFilter(MetricsFilter())
app.config.REQUEST_MAX_SIZE = PROPIC_MAX_FILE_SIZE * 3
app.ctx.om = OrderManager() app.ctx.om = OrderManager()
if FILL_CACHE: if FILL_CACHE:
checked, success = await app.ctx.om.update_cache(check_itemsQuestions=True) log.info("Filling cache!")
if checked and not success: await app.ctx.om.fill_cache()
logger.error(f"[{app.name}] Failure in app startup: An error occurred while loading items or questions or cache.") log.info("Cache fill done!")
app.stop()
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db') app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
@ -82,14 +60,7 @@ async def main_start(*_):
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True) app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
app.ctx.tpl.globals.update(time=time) app.ctx.tpl.globals.update(time=time)
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE) app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
app.ctx.tpl.globals.update(ROOM_DEADLINE=ROOM_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(int=int)
app.ctx.tpl.globals.update(len=len) app.ctx.tpl.globals.update(len=len)
@ -108,8 +79,9 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None):
if order and order.code != code: order = None if order and order.code != code: order = None
if not order: if not order:
res = await pretixClient.get(f"orders/{code}/", expectedStatusCodes=None) 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: if res.status_code != 200:
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.") raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")
@ -131,11 +103,6 @@ async def welcome(request, order: Order, quota: Quotas):
if not order: if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") 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 = [] pending_roommates = []
if order.pending_roommates: if order.pending_roommates:
for pr in order.pending_roommates: for pr in order.pending_roommates:
@ -159,7 +126,7 @@ async def welcome(request, order: Order, quota: Quotas):
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') tpl = app.ctx.tpl.get_template('welcome.html')
return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates, ROOM_ERROR_MESSAGES=ROOM_ERROR_TYPES, isSessionAdmin=await isSessionAdmin(request, order))) return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates))
@app.route("/manage/download_ticket") @app.route("/manage/download_ticket")
@ -171,76 +138,19 @@ async def download_ticket(request, order: Order):
if not order.status != 'confirmed': if not order.status != 'confirmed':
raise exceptions.Forbidden("You are not allowed to download this ticket.") raise exceptions.Forbidden("You are not allowed to download this ticket.")
res = await pretixClient.get(f"orders/{order.code}/download/pdf/", expectedStatusCodes=[200, 404, 409, 403]) async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, f"orders/{order.code}/download/pdf/"), headers=headers)
if res.status_code == 409 or res.status_code == 404: if res.status_code == 409:
raise exceptions.SanicException("Your ticket is still being generated. Please try again later!", status_code=res.status_code) raise exceptions.SanicException("Your ticket is still being generated. Please try again later!", status_code=res.status_code)
elif res.status_code == 403: 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) 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') 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") @app.route("/manage/logout")
async def logout(request): async def logour(request):
orgCode = request.cookies.get("foxo_code_ORG") raise exceptions.Forbidden("You have been logged out.", status_code=403)
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__": if __name__ == "__main__":
# Wait for pretix in server reboot app.run(host="0.0.0.0", port=8188, dev=DEV_MODE)
# 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,6 +10,7 @@ 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!") 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] 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') 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 sanic import Blueprint, exceptions, response
from random import choice from random import choice
from ext import * from ext import *
from config import headers, base_url_event from config import headers, base_url
from PIL import Image from PIL import Image
from os.path import isfile from os.path import isfile
from os import unlink from os import unlink
@ -11,8 +11,6 @@ from hashlib import sha224
from time import time from time import time
from urllib.parse import unquote from urllib.parse import unquote
import json import json
from metrics import *
import pretixClient
bp = Blueprint("checkin", url_prefix="/checkin") bp = Blueprint("checkin", url_prefix="/checkin")
@ -65,7 +63,8 @@ async def do_checkin(request):
await order.send_answers() await order.send_answers()
if not order.checked_in: if not order.checked_in:
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,]}) 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)
tpl = request.app.ctx.tpl.get_template('checkin_3.html') tpl = request.app.ctx.tpl.get_template('checkin_3.html')
return html(tpl.render(order=order, room_owner=room_owner, roommates=roommates)) return html(tpl.render(order=order, room_owner=room_owner, roommates=roommates))

View File

@ -1,156 +1,41 @@
from sanic.log import logging
LOG_LEVEL = logging.DEBUG
API_TOKEN = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
ORGANIZER = 'furizon' ORGANIZER = 'furizon'
EVENT_NAME = 'overlord' EVENT_NAME = 'river-side-2023'
HOSTNAME = 'reg.furizon.net' API_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxx'
SKIP_HEALTHCHECK = False HOSTNAME = 'your-pretix-hostname'
headers = {'Host': HOSTNAME, 'Authorization': f'Token {API_TOKEN}'} headers = {'Host': HOSTNAME, 'Authorization': f'Token {API_TOKEN}'}
domain = "http://urlllllllllllllllllllll/" base_url = f"https://{HOSTNAME}/api/v1/organizers/{ORGANIZER}/events/{EVENT_NAME}/"
base_url = "{domain}api/v1/"
base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/"
PROPIC_DEADLINE = 9999999999 PROPIC_DEADLINE = 1683575684
PROPIC_MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB FILL_CACHE = True
PROPIC_MAX_SIZE = (2048, 2048) # (Width, Height)
PROPIC_MIN_SIZE = (125, 125) # (Width, Height)
ROOM_DEADLINE = 9999999999 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
}
# This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id. # 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_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
TG_CHAT_ID = -1234567 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. # These order codes have additional functions.
ADMINS = ['XXXXX', 'YYYYY'] ADMINS = ['XXXXX', 'YYYYY']
# A list of staff_roles
ADMINS_PRETIX_ROLE_NAMES = ["Reserved Area admin", "main staff"]
SMTP_HOST = 'host' SMTP_HOST = 'your-smtp-host.com'
SMTP_PORT = 0 SMTP_USER = 'username'
SMTP_USER = 'user' SMTP_PASSWORD = 'password'
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 = { }

View File

@ -1,21 +0,0 @@
# 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

File diff suppressed because it is too large Load Diff

View File

@ -1,145 +0,0 @@
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
logger.info(f"[SMPT] killSmptClient: Lock status: {sslLock.locked()}")
sslTimer.cancel()
sslLock.acquire()
exp = None
if(smptSender is not None):
logger.debug('[SMPT] Closing smpt client')
try:
smptSender.quit() # it calls close() inside
except Exception as e:
exp = e
smptSender = None
sslLock.release()
if(exp != None):
raise exp
async def openSmptClient():
global sslLock
global sslTimer
global sslContext
global smptSender
logger.info(f"[SMPT] openSmptClient: Lock status: {sslLock.locked()}")
sslTimer.cancel()
sslLock.acquire()
exp = None
try:
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
except Exception as e:
exp = e
sslLock.release()
sslTimer = createTimer()
sslTimer.start()
if(exp != None):
raise exp
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']}'")
logger.info(f"[SMPT] sendEmail: Lock status: {sslLock.locked()}")
exp = None
sslLock.acquire()
try:
smptSender.sendmail(message['From'], message['to'], message.as_string())
except Exception as e:
exp = e
sslLock.release()
if(exp != None):
raise exp
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)

87
export.py Normal file
View File

@ -0,0 +1,87 @@
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)

308
ext.py
View File

@ -1,18 +1,11 @@
from dataclasses import dataclass from dataclasses import dataclass
from sanic import Request, exceptions, Sanic from sanic import Request, exceptions
import httpx import httpx
import re import re
from utils import *
from config import * from config import *
from os.path import join from os.path import join
import json import json
from sanic.log import logger
from time import time from time import time
from metrics import *
import asyncio
from threading import Lock
import pretixClient
import traceback
@dataclass @dataclass
class Order: class Order:
@ -20,12 +13,7 @@ class Order:
self.time = time() self.time = time()
self.data = data 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.status = {'n': 'pending', 'p': 'paid', 'e': 'expired', 'c': 'canceled'}[self.data['status']]
self.secret = data['secret']
if not len(self.data['positions']): if not len(self.data['positions']):
self.status = 'canceled' self.status = 'canceled'
@ -39,66 +27,42 @@ class Order:
self.sponsorship = None self.sponsorship = None
self.has_early = False self.has_early = False
self.has_late = False self.has_late = False
self.first_name = "None" self.first_name = None
self.last_name = "None" self.last_name = None
self.country = 'xx' self.country = 'xx'
self.address = None self.address = None
self.checked_in = False 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'] idata = data['invoice_address']
if idata: if idata:
self.address = f"{idata['street'].strip()} - {idata['zipcode'].strip()} {idata['city'].strip()} - {idata['country'].strip()}".replace("\n", "").replace("\r", "") self.address = f"{idata['street']} - {idata['zipcode']} {idata['city']} - {idata['country']}"
self.country = idata['country'] self.country = idata['country']
for p in self.data['positions']: for p in self.data['positions']:
if p['item'] in CATEGORIES_LIST_MAP['tickets']: if p['item'] in (ITEM_IDS['ticket'] + ITEM_IDS['daily']):
self.position_id = p['id'] self.position_id = p['id']
self.position_positionid = p['positionid'] self.position_positionid = p['positionid']
self.position_positiontypeid = p['item'] self.position_positiontypeid = p['item']
self.answers = p['answers'] 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.barcode = p['secret']
self.checked_in = bool(p['checkins']) self.checked_in = bool(p['checkins'])
if p['item'] in CATEGORIES_LIST_MAP['dailys']: if p['item'] in ITEM_IDS['membership_card']:
self.daily = True
self.dailyDays.append(CATEGORIES_LIST_MAP['dailys'].index(p['item']))
if p['item'] in CATEGORIES_LIST_MAP['memberships']:
self.has_card = True self.has_card = True
if p['item'] == ITEMS_ID_MAP['sponsorship_item']: if p['item'] in ITEM_IDS['sponsorship']:
sponsorshipType = key_from_value(ITEM_VARIATIONS_MAP['sponsorship_item'], p['variation']) self.sponsorship = 'normal' if p['variation'] == ITEMS_IDS['sponsorship'][0] else 'super'
self.sponsorship = sponsorshipType[0].replace ('sponsorship_item_', '') if len(sponsorshipType) > 0 else None
if p['attendee_name']: if p['attendee_name']:
self.first_name = p['attendee_name_parts']['given_name'] self.first_name = p['attendee_name_parts']['given_name']
self.last_name = p['attendee_name_parts']['family_name'] self.last_name = p['attendee_name_parts']['family_name']
if p['item'] == ITEMS_ID_MAP['early_arrival_admission']: if p['item'] == ITEM_IDS['early_arrival']:
self.has_early = True self.has_early = True
if p['item'] == ITEMS_ID_MAP['late_departure_admission']: if p['item'] == ITEM_IDS['late_departure']:
self.has_late = True 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.total = float(data['total'])
self.fees = 0 self.fees = 0
self.refunds = 0 self.refunds = 0
@ -113,12 +77,6 @@ class Order:
self.payment_provider = data['payment_provider'] self.payment_provider = data['payment_provider']
self.comment = data['comment'] self.comment = data['comment']
self.phone = data['phone'] 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.shirt_size = self.ans('shirt_size')
self.is_artist = True if self.ans('is_artist') != 'No' else False self.is_artist = True if self.ans('is_artist') != 'No' else False
self.is_fursuiter = True if self.ans('is_fursuiter') != 'No' else False self.is_fursuiter = True if self.ans('is_fursuiter') != 'No' else False
@ -139,21 +97,17 @@ class Order:
self.pending_room = self.ans('pending_room') self.pending_room = self.ans('pending_room')
self.pending_roommates = self.ans('pending_roommates').split(',') if self.ans('pending_roommates') else [] 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_members = self.ans('room_members').split(',') if self.ans('room_members') else []
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_owner = (self.code == self.room_id)
self.room_secret = self.ans('room_secret') self.room_secret = self.ans('room_secret')
self.app_token = self.ans('app_token') self.app_token = self.ans('app_token')
self.nfc_id = self.ans('nfc_id') self.nfc_id = self.ans('nfc_id')
self.can_scan_nfc = True if self.ans('can_scan_nfc') != 'No' else False self.can_scan_nfc = True if self.ans('can_scan_nfc') != 'No' else False
self.actual_room = self.ans('actual_room') 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.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): def __getitem__(self, var):
return self.data[var] return self.data[var]
def set_room_errors (self, to_set):
self.room_errors = to_set
def ans(self, name): def ans(self, name):
for p in self.data['positions']: for p in self.data['positions']:
for a in p['answers']: for a in p['answers']:
@ -163,131 +117,58 @@ class Order:
return a['answer'] return a['answer']
return None 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): async def edit_answer(self, name, new_answer):
found = False found = False
self.pending_update = True self.pending_update = True
for key in range(len(self.answers)): for key in range(len(self.answers)):
if self.answers[key].get('question_identifier', None) == name: if self.answers[key].get('question_identifier', None) == name:
if new_answer != None: if new_answer != None:
if DEV_MODE and EXTRA_PRINTS: logger.debug('[ANSWER EDIT] EXISTING ANSWER UPDATE %s => %s', name, new_answer) print('EXISTING ANSWER UPDATE', name, '=>', new_answer)
self.answers[key]['answer'] = new_answer self.answers[key]['answer'] = new_answer
found = True found = True
else: else:
if DEV_MODE and EXTRA_PRINTS: logger.debug('[ANSWER EDIT] DEL ANSWER %s => %s', name, new_answer) print('DEL ANSWER', name, '=>', new_answer)
del self.answers[key] del self.answers[key]
break break
if (not found) and (new_answer is not None): if (not found) and (new_answer is not None):
res = await pretixClient.get("questions/")
async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, 'questions/'), headers=headers)
res = res.json() res = res.json()
for r in res['results']: for r in res['results']:
if r['identifier'] != name: continue if r['identifier'] != name: continue
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'[ANSWER EDIT] %s => %s', name, new_answer) print('ANSWER UPDATE', name, '=>', new_answer)
self.answers.append({ self.answers.append({
'question': r['id'], 'question': r['id'],
'answer': new_answer, 'answer': new_answer,
'options': r['options'] 'options': r['options']
}) })
self.loadAns()
async def send_answers(self): async def send_answers(self):
if DEV_MODE and EXTRA_PRINTS: logger.debug("[ANSWER POST] POSITION ID IS %s", self.position_id) async with httpx.AsyncClient() as client:
print("POSITION ID IS", self.position_id)
for i, ans in enumerate(self.answers): 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 # Fix for karaoke fields
#if ans['question'] == 40: if ans['question'] == 40:
# del self.answers[i]['options'] del self.answers[i]['options']
# del self.answers[i]['option_identifiers'] del self.answers[i]['option_identifiers']
ans = [] if self.status == "canceled" else self.answers res = await client.patch(join(base_url, f'orderpositions/{self.position_id}/'), headers=headers, json={'answers': self.answers})
res = await pretixClient.patch(f'orderpositions/{self.position_id}/', json={'answers': ans}, expectedStatusCodes=None)
if res.status_code != 200: if res.status_code != 200:
e = res.json()
if "answers" in e:
for ans, err in zip(self.answers, res.json()['answers']): for ans, err in zip(self.answers, res.json()['answers']):
if err: if err:
logger.error ('[ANSWERS SENDING] ERROR ON %s %s', ans, err) print('ERROR ON', 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.pending_update = False
self.time = -1 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:
ret : Quota = None
for q in QUOTA_LIST:
if (q.has_item(item, variation)):
if(ret == None or (q.size != None and q.size < ret.size)):
ret = q
return ret
@dataclass @dataclass
class Quotas: class Quotas:
@ -301,143 +182,48 @@ class Quotas:
return 0 return 0
async def get_quotas(request: Request=None): async def get_quotas(request: Request=None):
res = await pretixClient.get('quotas/?order=id&with_availability=true') async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, 'quotas/?order=id&with_availability=true'), headers=headers)
res = res.json() res = res.json()
return Quotas(res) 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()
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): async def get_order(request: Request=None):
await request.receive_body() await request.receive_body()
return await request.app.ctx.om.get_order(request=request) return await request.app.ctx.om.get_order(request=request)
class OrderManager: class OrderManager:
def __init__(self): def __init__(self):
self.lastCacheUpdate = 0
self.updating : Lock = Lock()
self.empty()
def empty(self):
self.cache = {} self.cache = {}
self.order_list = [] self.order_list = []
# Will fill cache once the last cache update is greater than cache expire time def add_cache(self, order):
async def update_cache(self, check_itemsQuestions=False): self.cache[order.code] = order
t = time() if not order.code in self.order_list:
to_return = False self.order_list.append(order.code)
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 add_cache(self, order, cache=None, orderList=None): def remove_cache(self, code):
# Extra params for dry runs if code in self.cache:
if(cache is None): del self.cache[code]
cache = self.cache self.order_list.remove(code)
if(orderList is None):
orderList = self.order_list
cache[order.code] = order async def fill_cache(self):
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, check_itemsQuestions=False) -> bool:
# Check cache lock
logger.info(f"[CACHE] Lock status: {self.updating.locked()}")
self.updating.acquire()
ret = False
exp = None
try:
ret = await self.fill_cache_INTERNAL(check_itemsQuestions=check_itemsQuestions)
except Exception as e:
exp = e
self.updating.release()
logger.info(f"[CACHE] Ret status: {ret}. Exp: {exp}")
if(exp != None):
raise exp
return ret
async def fill_cache_INTERNAL(self, check_itemsQuestions=False) -> bool:
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 p = 0
try:
async with httpx.AsyncClient() as client:
while 1: while 1:
p += 1 p += 1
res = await pretixClient.get(f"orders/?page={p}", expectedStatusCodes=[200, 404]) res = await client.get(join(base_url, f"orders/?page={p}"), headers=headers)
if res.status_code == 404: break if res.status_code == 404: break
# Parse order data
data = res.json() data = res.json()
for o in data['results']: for o in data['results']:
o = Order(o) o = Order(o)
if o.status in ['canceled', 'expired']: if o.status in ['canceled', 'expired']:
self.remove_cache(o.code, cache=cache, orderList=orderList) self.remove_cache(o.code)
else: else:
self.add_cache(Order(o), cache=cache, orderList=orderList) self.add_cache(Order(o))
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
# 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): async def get_order(self, request=None, code=None, secret=None, nfc_id=None, cached=False):
@ -447,7 +233,6 @@ class OrderManager:
if order.nfc_id == nfc_id: if order.nfc_id == nfc_id:
return order return order
await self.update_cache()
# If a cached order is needed, just get it if available # 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: if code and cached and code in self.cache and time()-self.cache[code].time < 3600:
return self.cache[code] return self.cache[code]
@ -458,9 +243,10 @@ class OrderManager:
secret = request.cookies.get("foxo_secret") 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)): if re.match('^[A-Z0-9]{5}$', code or '') and (secret is None or re.match('^[a-z0-9]{16,}$', secret)):
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'Fetching {code} with secret {secret}') print('Fetching', code, 'with secret', secret)
res = await pretixClient.get(f"orders/{code}/", expectedStatusCodes=None) async with httpx.AsyncClient() as client:
res = await client.get(join(base_url, f"orders/{code}/"), headers=headers)
if res.status_code != 200: if res.status_code != 200:
if request: if request:
raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.") raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.")

View File

@ -1,92 +0,0 @@
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

@ -3,7 +3,6 @@ from sanic import Blueprint, exceptions, response
from ext import * from ext import *
from urllib.parse import unquote from urllib.parse import unquote
from config import ADMINS from config import ADMINS
from utils import isSessionAdmin
import json import json
bp = Blueprint("karaoke", url_prefix="/manage/karaoke") bp = Blueprint("karaoke", url_prefix="/manage/karaoke")
@ -11,7 +10,7 @@ bp = Blueprint("karaoke", url_prefix="/manage/karaoke")
@bp.get("/admin") @bp.get("/admin")
async def show_songs(request, order: Order): async def show_songs(request, order: Order):
if not await isSessionAdmin(request, order): if order.code not in ADMINS:
raise exceptions.Forbidden("Birichino") raise exceptions.Forbidden("Birichino")
orders = [x for x in request.app.ctx.om.cache.values() if x.karaoke_songs] orders = [x for x in request.app.ctx.om.cache.values() if x.karaoke_songs]
@ -29,7 +28,7 @@ async def show_songs(request, order: Order):
@bp.post("/approve") @bp.post("/approve")
async def approve_songs(request, order: Order): async def approve_songs(request, order: Order):
if not await isSessionAdmin(request, order): if order.code not in ADMINS:
raise exceptions.Forbidden("Birichino") raise exceptions.Forbidden("Birichino")
for song in request.form: for song in request.form:
@ -45,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 not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if not await isSessionAdmin(request, order): if order.code not in ADMINS:
raise exceptions.Forbidden("Birichino") raise exceptions.Forbidden("Birichino")
songname = unquote(songname) songname = unquote(songname)

View File

@ -1,43 +0,0 @@
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'
}
}

View File

@ -1,89 +0,0 @@
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)

View File

@ -1,50 +0,0 @@
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,32 +6,9 @@ from PIL import Image
from io import BytesIO from io import BytesIO
from hashlib import sha224 from hashlib import sha224
from time import time from time import time
from utils import isSessionAdmin
import os
bp = Blueprint("propic", url_prefix="/manage/propic") 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") @bp.post("/upload")
async def upload_propic(request, order: Order): 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!") if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
@ -39,15 +16,13 @@ async def upload_propic(request, order: Order):
if order.propic_locked: if order.propic_locked:
raise exceptions.BadRequest("You have been limited from further editing the propic.") raise exceptions.BadRequest("You have been limited from further editing the propic.")
if request.form.get('submit') != 'Upload' and (time() > PROPIC_DEADLINE and not await isSessionAdmin(request, order)): if request.form.get('submit') != 'Upload' and time() > PROPIC_DEADLINE:
raise exceptions.BadRequest("The deadline has passed. You cannot modify the badges at this moment.") raise exceptions.BadRequest("The deadline has passed. You cannot modify the badges at this moment.")
if request.form.get('submit') == 'Delete main image': if request.form.get('submit') == 'Delete main image':
await resetDefaultPropic(request, order, False, sendAnswer=False) await order.edit_answer('propic', None)
await order.edit_answer('propic', None) #This MUST come after the reset default propic!
elif request.form.get('submit') == 'Delete fursuit image': elif request.form.get('submit') == 'Delete fursuit image':
await resetDefaultPropic(request, order, True, sendAnswer=False) await order.edit_answer('propic_fursuiter', None)
await order.edit_answer('propic_fursuiter', None) #This MUST come after the reset default propic!
else: else:
for fn, body in request.files.items(): for fn, body in request.files.items():
if fn not in ['propic', 'propic_fursuiter']: if fn not in ['propic', 'propic_fursuiter']:
@ -55,33 +30,15 @@ async def upload_propic(request, order: Order):
if not body[0].body: continue if not body[0].body: continue
# Check max file size h = sha224(body[0].body).hexdigest()[:32]
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: try:
bodyBytesBuff = BytesIO(body[0].body) img = Image.open(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: with open(f"res/propic/{fn}_{order.code}_original", "wb") as f:
f.write(body[0].body) f.write(body[0].body)
width, height = img.size
aspect_ratio = width/height aspect_ratio = width/height
if aspect_ratio > 1: if aspect_ratio > 1:
crop_amount = (width - height) / 2 crop_amount = (width - height) / 2
@ -91,31 +48,12 @@ async def upload_propic(request, order: Order):
img = img.crop((0, crop_amount, width, height - crop_amount)) img = img.crop((0, crop_amount, width, height - crop_amount))
img = img.convert('RGB') img = img.convert('RGB')
width, height = img.size
img.thumbnail((512,512)) img.thumbnail((512,512))
imgBytesBuff = BytesIO() img.save(f"res/propic/{fn}_{order.code}_{h}.jpg")
img.save(imgBytesBuff, format='jpeg') except:
imgBytes = imgBytesBuff.getvalue() raise exceptions.BadRequest("The image you uploaded is not valid.")
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: else:
await order.edit_answer(fn, f"{fn}_{order.code}.jpg") await order.edit_answer(fn, f"{fn}_{order.code}_{h}.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() await order.send_answers()
return redirect("/manage/welcome#badge") return redirect("/manage/welcome#badge")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

View File

@ -1,21 +0,0 @@
<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>

View File

@ -1,147 +0,0 @@
<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

@ -1,117 +0,0 @@
<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.

Before

Width:  |  Height:  |  Size: 280 KiB

View File

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

View File

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

BIN
res/botbg2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
res/error_openbox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 297 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 466 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 386 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 188 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 300 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

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

Before

Width:  |  Height:  |  Size: 125 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 310 B

View File

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,21 +0,0 @@
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');
}

View File

@ -1,23 +0,0 @@
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

@ -1,31 +0,0 @@
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

@ -1,213 +0,0 @@
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 ();

View File

@ -1,53 +0,0 @@
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;
}

View File

@ -1,54 +0,0 @@
/* 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;
}
}

View File

@ -1,91 +0,0 @@
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;
}
}

View File

@ -1,113 +0,0 @@
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;
}

View File

@ -1,20 +0,0 @@
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;
}
}

View File

@ -1,80 +0,0 @@
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;
}
}

140
room.py
View File

@ -3,21 +3,9 @@ from sanic import Blueprint, exceptions
from random import choice from random import choice
from ext import * from ext import *
from config import headers from config import headers
import os
from image_util import generate_room_preview, get_room
from utils import confirm_room_by_order
from time import time
bp = Blueprint("room", url_prefix="/manage/room") bp = Blueprint("room", url_prefix="/manage/room")
@bp.middleware
async def deadline_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 time() > ROOM_DEADLINE and not await isSessionAdmin(request, order):
raise exceptions.BadRequest("The deadline has passed. You cannot modify the room at this moment.")
@bp.post("/create") @bp.post("/create")
async def room_create_post(request, order: Order): async def room_create_post(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: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
@ -31,9 +19,6 @@ async def room_create_post(request, order: Order):
if order.room_id: 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." 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: if not error:
await order.edit_answer('room_name', name) await order.edit_answer('room_name', name)
await order.edit_answer('room_id', order.code) await order.edit_answer('room_id', order.code)
@ -50,9 +35,6 @@ 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 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') tpl = request.app.ctx.tpl.get_template('create_room.html')
return html(tpl.render(order=order)) return html(tpl.render(order=order))
@ -61,7 +43,7 @@ async def delete_room(request, order: Order):
if not order: if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if not order.room_owner: if order.room_id != order.code:
raise exceptions.BadRequest("You are not allowed to delete room of others.") raise exceptions.BadRequest("You are not allowed to delete room of others.")
if order.ans('room_confirmed'): if order.ans('room_confirmed'):
@ -75,7 +57,6 @@ async def delete_room(request, order: Order):
await order.edit_answer('room_members', None) await order.edit_answer('room_members', None)
await order.edit_answer('room_secret', None) await order.edit_answer('room_secret', None)
await order.send_answers() await order.send_answers()
remove_room_preview (order.code)
return redirect('/manage/welcome') return redirect('/manage/welcome')
@bp.post("/join") @bp.post("/join")
@ -90,9 +71,6 @@ async def join_room(request, order: Order):
if order.room_id: if order.room_id:
raise exceptions.BadRequest("You are in another room already. Why would you join another?") 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() code = request.form.get('code').strip()
room_secret = request.form.get('room_secret').strip() room_secret = request.form.get('room_secret').strip()
@ -110,9 +88,6 @@ async def join_room(request, order: Order):
if room_owner.room_confirmed: if room_owner.room_confirmed:
raise exceptions.BadRequest("The room you're trying to join has been confirmed already") 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): #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...") #raise exceptions.BadRequest("What? You should never reach this check, but whatever...")
@ -125,7 +100,7 @@ async def join_room(request, order: Order):
await room_owner.edit_answer('pending_roommates', ','.join(pending_roommates)) await room_owner.edit_answer('pending_roommates', ','.join(pending_roommates))
await room_owner.send_answers() await room_owner.send_answers()
remove_room_preview (code)
return redirect('/manage/welcome') return redirect('/manage/welcome')
@bp.route("/kick/<code>") @bp.route("/kick/<code>")
@ -148,7 +123,7 @@ async def kick_member(request, code, order: Order):
await order.send_answers() await order.send_answers()
await to_kick.send_answers() await to_kick.send_answers()
remove_room_preview (order.code)
return redirect('/manage/welcome') return redirect('/manage/welcome')
@bp.route("/renew_secret") @bp.route("/renew_secret")
@ -190,9 +165,6 @@ async def approve_roomreq(request, code, order: Order):
if not order: if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") 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: if not code in order.pending_roommates:
raise exceptions.BadRequest("You cannot accept people that didn't request to join your room") raise exceptions.BadRequest("You cannot accept people that didn't request to join your room")
@ -215,7 +187,7 @@ async def approve_roomreq(request, code, order: Order):
await pending_member.send_answers() await pending_member.send_answers()
await order.send_answers() await order.send_answers()
remove_room_preview(order.code)
return redirect('/manage/welcome') return redirect('/manage/welcome')
@bp.route("/leave") @bp.route("/leave")
@ -239,7 +211,7 @@ async def leave_room(request, order: Order):
await room_owner.send_answers() await room_owner.send_answers()
await order.send_answers() await order.send_answers()
remove_room_preview (order.room_id)
return redirect('/manage/welcome') return redirect('/manage/welcome')
@bp.route("/reject/<code>") @bp.route("/reject/<code>")
@ -247,9 +219,6 @@ async def reject_roomreq(request, code, order: Order):
if not order: if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") 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: if not code in order.pending_roommates:
raise exceptions.BadRequest("You cannot reject people that didn't request to join your room") raise exceptions.BadRequest("You cannot reject people that didn't request to join your room")
@ -272,32 +241,6 @@ async def reject_roomreq(request, code, order: Order):
return redirect('/manage/welcome') 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") @bp.route("/confirm")
async def confirm_room(request, order: Order, quotas: Quotas): async def confirm_room(request, order: Order, quotas: Quotas):
if not order: if not order:
@ -309,31 +252,54 @@ async def confirm_room(request, order: Order, quotas: Quotas):
if order.room_id != order.code: if order.room_id != order.code:
raise exceptions.BadRequest("You are not allowed to confirm rooms of others.") raise exceptions.BadRequest("You are not allowed to confirm rooms of others.")
# This is not needed anymore you buy tickets already if quotas.get_left(len(order.room_members)) == 0:
#if quotas.get_left(len(order.room_members)) == 0: raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
# raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
await confirm_room_by_order(order, request) 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()
return redirect('/manage/welcome') 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))

View File

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

View File

@ -1,41 +1,12 @@
from sanic.response import html from sanic.response import html
from sanic import Blueprint, Request from sanic import Blueprint
from messages import NOSECOUNT
from ext import * from ext import *
bp = Blueprint("stats", url_prefix="/manage") bp = Blueprint("stats", url_prefix="/manage")
@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('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") @bp.route("/nosecount")
async def nose_count(request: Request, order: Order): async def nose_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: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e']} 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') 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)) return html(tpl.render(orders=orders, order=order))

View File

@ -1,78 +0,0 @@
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)

View File

@ -1,7 +0,0 @@
#!/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

@ -1,8 +0,0 @@
#!/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

View File

@ -1,66 +0,0 @@
#!/usr/bin/python
from os import listdir, remove, getenv
from os.path import isfile, join
import datetime
import subprocess
PRETIX_BACKUP = False
WEBINT_BACKUP = False
WP_BACKUP = False
STUFF_BACKUP = False
BACKUP_DIR_PRETIX = "/home/pretix/backups/"
BACKUP_DIR_WEBINT = "/home/webint/backups/"
BACKUP_DIR_WP = "/home/worcopio/backups/"
BACKUP_DIR_STUFF = "/root/backups/"
MAX_FILE_NO = 7
COMMAND_PRETIX_POSTGRES = "pg_dump -F p pretix | gzip > %s" # Restore with psql -f %s
COMMAND_PRETIX_DATA = "tar -czf %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 -czf %s /home/webint/furizon_webint" # Restore with tar -xvf %s
COMMAND_WP_MYSQL = "mysqldump -h 127.0.0.1 -P 5688 -u root --password=__PASSWORD__ --all-databases | gzip > %s" # Restore with zcat %s | mysql -h 127.0.0.1 -P 5688 -u root
COMMAND_WP_DATA = "tar -czf %s /var/lib/docker/volumes/worcopio-docker_wordpress/_data" # Restore with tar -xvf %s
COMMAND_STUFF = "tar -czf %s /etc/ /var/backups/ /var/log/ /var/mail/ /var/pretix-data/ /var/prometheus-data/ /var/spool/ /var/www/ /var/lib/grafana/ /var/lib/redis/" # 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)
if(WP_BACKUP):
mysqlCmd = COMMAND_WP_MYSQL
mysqlPwd = getenv("MYSQL_PWD")
if(mysqlPwd != None and mysqlPwd.strip() != ""):
mysqlPwd = mysqlPwd.strip()
print(f"Running with password `{mysqlPwd}`")
mysqlCmd = mysqlCmd.replace("__PASSWORD__", mysqlPwd)
runBackup("wp_mysql", "backup.sql.gz", join(BACKUP_DIR_WP, "mysql"), mysqlCmd)
else:
print("Backup run without the $MYSQL_PWD env var set. Skipping wp_mysql backup")
runBackup("wp_wp", "backup.tar.gz", join(BACKUP_DIR_WP, "wp"), COMMAND_WP_DATA)
if(STUFF_BACKUP):
runBackup("stuff", "backup.tar.gz", BACKUP_DIR_STUFF, COMMAND_STUFF)

View File

@ -1,91 +0,0 @@
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)

View File

@ -1,11 +0,0 @@
# python merda
import asyncio
async def a():
print("a")
def b():
loop = asyncio.get_event_loop()
print(loop)
b()

View File

@ -1,32 +0,0 @@
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")

View File

@ -1,31 +0,0 @@
{% 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,15 +1,21 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
{% block head %}{% endblock %}
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<meta name="viewport" content="width=400rem" /> <meta name="viewport" content="width=400rem" />
<meta name="supported-color-schemes" content="light dark"> <meta name="supported-color-schemes" content="light dark">
<link rel="stylesheet" href="/res/pico.min.css"> <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> <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 div {text-align:center;}
.people h3, .people p, .people h5 {margin:0;} .people h3, .people p, .people h5 {margin:0;}
.people {grid-auto-flow:row;} .people {grid-auto-flow:row;}
@ -17,12 +23,14 @@
main {min-height: 30em;} main {min-height: 30em;}
mark {background:#0f0;}
h1 img {max-height:1.4em;} h1 img {max-height:1.4em;}
.notice {padding:0.8em;border-radius:3px;background:#e53935;color:#eee;} .notice {padding:0.8em;border-radius:3px;background:#e53935;color:#eee;}
.notice a {color:#eee;text-decoration:underline;} .notice a {color:#eee;text-decoration:underline;}
.container {max-width:50em;padding:1em;box-sizing:border-box;} .container {max-width:40em;padding:1em;box-sizing:border-box;}
td > a[role=button] {padding: 0.3em 0.7em;} td > a[role=button] {padding: 0.3em 0.7em;}
td {padding-left: 0.2em;padding-right: 0.2em;} td {padding-left: 0.2em;padding-right: 0.2em;}
a[role=button] {margin: 0.25em 0;}
td > input[type=file] {margin:0;padding:0;} td > input[type=file] {margin:0;padding:0;}
section {margin-bottom:3em;} section {margin-bottom:3em;}
@media (min-width: 500px) {body .grid {grid-template-columns: repeat(auto-fit, minmax(0%, 1fr));}} @media (min-width: 500px) {body .grid {grid-template-columns: repeat(auto-fit, minmax(0%, 1fr));}}
@ -32,7 +40,14 @@
summary[role=button] {background:var(--primary-focus);color:var(--contrast);} summary[role=button] {background:var(--primary-focus);color:var(--contrast);}
summary img, td img {height:1.2em;width:2em;box-sizing: border-box;} 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 } body .grid.people { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)) !important }
.status {float:right;} .status {float:right;}
@ -42,16 +57,41 @@
footer img {height:1.3em;} 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;} .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> </style>
<script src="/res/scripts/base.js" defer></script>
<!-- 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 -->
</head> </head>
<body onscroll="onScrollNav()"> <body>
{% include 'blocks/navbar.html' %} <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>
{% block main %}{% endblock %} {% block main %}{% endblock %}
<script type="text/javascript"> <script type="text/javascript">
@ -66,7 +106,7 @@
}); });
</script> </script>
<footer> <footer>
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> Made in 🇮🇹 by <img src="/res/icons/silhouette.svg" class="icon" /> · <a href="/manage/privacy">Privacy</a>
</footer> </footer>
</body> </body>
</html> </html>

View File

@ -1,51 +1,58 @@
<details id="badge"> <details id="badge">
<summary role="button"><img src="/res/icons/badge.svg" class="icon"/>Badge Customization {% if not order.isBadgeValid() %}<span class="status">⚠️</span>{% endif %}</summary> <summary role="button"><img src="/res/icons/badge.svg" class="icon"/>Badge Customization {% if not order.ans('propic') %}<span class="status">⚠️</span>{% endif %}</summary>
{# Badge is always shown #} {# Badge is always shown #}
<h2>Badge</h2> <h2>Badge</h2>
{% if order.propic_locked %} {% if order.propic_locked %}
<p class="notice">⚠️ You have been limited from further editing your profile pic.</p> <p class="notice">⚠️ You have been limited from further editing your profile pic.</p>
{% endif %} {% endif %}
{% if not order.ans('propic') or (order.is_fursuiter and not order.ans('propic_fursuiter')) %} {% if (not order.ans('propic')) or (order.ans('is_fursuiter') != 'No' 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> <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 %} {% endif %}
<form method="POST" enctype="multipart/form-data" action="/manage/propic/upload"> <form method="POST" enctype="multipart/form-data" action="/manage/propic/upload">
<div class="grid" style="text-align:center;margin-bottom:1em;"> <div class="grid" style="text-align:center;margin-bottom:1em;">
<div> <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') %} {% if not order.ans('propic') %}
<input type="file" value="" accept="image/jpeg,image/png" name="propic" /> <input type="file" value="" accept="image/jpeg,image/png" name="propic" />
{% endif %} {% else %}
<div class="propic-container">
<img src="/res/propic/{{order.ans('propic') or 'default.png'}}" class="propic" />
</div> </div>
{% if order.is_fursuiter %} {% endif %}
<p>Normal Badge</p>
</div>
{% if order.ans('is_fursuiter') != 'No' %}
<div> <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') %} {% if not order.ans('propic_fursuiter') %}
<input type="file" value="" accept="image/jpeg,image/png" name="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 %} {% endif %}
<p>Fursuit Badge</p>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% if time() > PROPIC_DEADLINE and not isSessionAdmin %} {% if time() > PROPIC_DEADLINE %}
<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> <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 %} {% else %}
<p><em> <p><em>
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 /> Min size: 64x64 - Max Size: 5MB, 2048x2048 - 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. 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> </em></p>
{% endif %} {% endif %}
<div class="grid grid_2x2"> <div class="grid">
<input style="grid-area: 1 / 1 / 2 / 3;" type="submit" name="submit" value="Upload" {{'disabled' if ((order.ans('propic') and order.ans('propic_fursuiter'))) or (time() > PROPIC_DEADLINE and not isSessionAdmin) else ''}} /> {% if order.ans('propic') %}
<input style="grid-area: 2 / 1 / 3 / 2;" type="submit" name="submit" value="Delete main image" {{'disabled' if ((time() > PROPIC_DEADLINE and not isSessionAdmin) or not order.ans('propic')) else ''}} /> <input type="submit" name="submit" value="Delete main image" {{'disabled' if time() > PROPIC_DEADLINE else ''}} />
<input style="grid-area: 2 / 2 / 3 / 3;" type="submit" name="submit" value="Delete fursuit image" {{'disabled' if ((time() > PROPIC_DEADLINE and not isSessionAdmin) or not order.ans('propic_fursuiter')) 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> </div>
</form> </form>
</details> </details>

View File

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

View File

@ -1,21 +0,0 @@
<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,35 +1,21 @@
<details id="room"> <details id="room">
<summary role="button"><img src="/res/icons/bedroom.svg" class="icon"/> Accomodation & Roommates {% if not order.room_confirmed %}<span class="status">⚠️</span>{% endif %}</summary> <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 style="margin-bottom:0;">Your room {% if room_members %}- {{room_members[0].ans('room_name')}}{% endif %}</h2> <h2>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 %}
{% if time() > ROOM_DEADLINE %}
<p class="notice">⚠️ The deadline to edit your room has passed. If your room is not full it will be subject to changes by the staff as we optimize for hotel capacity.</p>
{% else %}
{# Show alert if room owner has wrong people inside #} {# 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) %} #} {% 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> #} <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 %} #} {% endif %}
{# Show alert if room was not confirmed #} {# Show alert if room was not confirmed #}
{% if order.room_id and not order.room_confirmed %} {% if order.room_id and not order.room_confirmed %}
<p class="notice">⚠️ Your room hasn't been confirmed yet. Unconfirmed rooms are subject to changes by the staff as we optimize for hotel capacity.</p> <p class="notice">⚠️ Your room hasn't been confirmed yet. Unconfirmed rooms are subject to changes by the staff as we optimize for hotel capacity.</p>
{% endif %} {% endif %}
{% endif %}
{# Show notice if the room is confirmed #} {# Show notice if the room is confirmed #}
{% if order.room_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 %} {% endif %}
{# Show roommates if room is set #} {# Show roommates if room is set #}
@ -38,16 +24,17 @@
<div class="grid people" style="padding-bottom:1em;"> <div class="grid people" style="padding-bottom:1em;">
{% for person in room_members %} {% for person in room_members %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %} <div class="propic-container">
{% include 'blocks/propic.html' %} <img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
{% endwith %} <img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
</div>
<h3>{{person.ans('fursona_name')}}</h3> <h3>{{person.ans('fursona_name')}}</h3>
{% if person.code == order.room_id %}<p><strong style="color:#c6f">ROOM OWNER</strong></p>{% endif %} {% 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.is_fursuiter}}</p> <p>{{person.ans('staff_title') if person.ans('staff_title') else ''}} {{'Fursuiter' if person.ans('is_fursuiter') != 'No'}}</p>
{% if person.status == 'pending' %} {% if person.status == 'pending' %}
<p><strong style="color:red;">UNPAID</strong></p> <p><strong style="color:red;">UNPAID</strong></p>
{% endif %} {% endif %}
{% if order.room_owner and person.code != order.code and (not order.room_confirmed) and (time() <= ROOM_DEADLINE or isSessionAdmin) %}<a href="/manage/room/kick/{{person.code}}">KICK</a>{% endif %} {% if order.room_owner and person.code != order.code and (not order.room_confirmed) %}<a href="/manage/room/kick/{{person.code}}">KICK</a>{% endif %}
</div> </div>
{% if person.status != 'paid' %} {% if person.status != 'paid' %}
@ -55,10 +42,9 @@
{% endif %} {% endif %}
{% endfor %} {% 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> <div>
<a {% if time() <= ROOM_DEADLINE or isSessionAdmin %} href="javascript:document.getElementById('modal-roominvite').setAttribute('open', 'true');" {% else %} disabled {% endif %}> <a href="javascript:document.getElementById('modal-roominvite').setAttribute('open', 'true');">
<div class="propic-container"> <div class="propic-container">
<img class="propic" src="/res/new.png" /> <img class="propic" src="/res/new.png" />
<h3>Invite</h3> <h3>Invite</h3>
@ -70,13 +56,13 @@
</div> </div>
{% elif order.pending_room %} {% elif order.pending_room %}
<p>You have have asked to join the room of another member. Wait for them to confirm or reject your request.</p> <p>You have have asked to join the room of another member. Wait for them to confirm or reject your request.</p>
<a role="button" href="/manage/room/cancel_request" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}}>Cancel pending join request</a> <a role="button" href="/manage/room/cancel_request">Cancel pending join request</a>
{% else %} {% else %}
<p class="notice">🎲 If you don't join a room or create your one within the room deadline, we will randomly put you into a room with free spots.</p> <p class="notice">🎲 If you don't join a room or create your one within the room deadline, we will randomly put you into a room with free spots.</p>
<p>To join a room, ask somebody to send you their room code.</p> <p>To join a room, ask somebody to send you their room code.</p>
<p class="grid"> <p class="grid">
<a role="button" href="/manage/room/create" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}}>Create a room</a> <a role="button" href="/manage/room/create">Create a room</a>
<a role="button" href="javascript:document.getElementById('modal-joinroom').setAttribute('open', 'true');" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}}>Join a room</a> <a role="button" href="javascript:document.getElementById('modal-joinroom').setAttribute('open', 'true');">Join a room</a>
</p> </p>
{% endif %} {% endif %}
@ -86,21 +72,18 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
<p class="grid grid_2x2"> <p class="grid">
{% if order.room_owner %} {% 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 %} {% if not order.room_confirmed %}
{# <a role="button" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}} {% 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');" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}}>Rename room</a>
<a style="grid-area: 1 / 2 / 2 / 3;" href="/manage/room/delete" role="button" {{'disabled' if (len(room_members) > 1) or (time() > ROOM_DEADLINE and not isSessionAdmin) else ''}} >Delete room</a>
<a style="grid-area: 2 / 1 / 3 / 3; display:block;" role="button" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}} {% 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" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}} 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 %} {% endif %}
{% else %} {% else %}
{% if order.room_id and not order.room_confirmed %} {% if order.room_id and not order.room_confirmed %}
<a href="/manage/room/leave" role="button" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}}>Leave room</a> <a href="/manage/room/leave" role="button">Leave room</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</p> </p>
@ -117,10 +100,8 @@
{% if person.status == 'pending' %} {% if person.status == 'pending' %}
<td><strong style="color:red;">UNPAID</strong></td> <td><strong style="color:red;">UNPAID</strong></td>
{% endif %} {% endif %}
{% 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/approve/{{person.code}}" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}}>Approve</a></td> <td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/reject/{{person.code}}">Reject</a></td>
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/reject/{{person.code}}" {{'disabled' if time() > ROOM_DEADLINE and not isSessionAdmin else ''}}>Reject</a></td>
{% endif %}
</tr> </tr>
</div> </div>
@ -132,13 +113,13 @@
{% endif %} {% endif %}
{# Room availability is always shown #} {# Room availability is always shown #}
{# <h4>Room availability</h4> #} <h4>Room availability</h4>
{# <table> #} <table>
{# {% for q in quota.data['results'] if 'Room' in q['name'] %} #} {% for q in quota.data['results'] if 'Room' in q['name'] %}
{# <tr {% if q['available_number'] == 0 %}style="text-decoration:line-through;"{% endif %}> #} <tr {% if q['available_number'] == 0 %}style="text-decoration:line-through;"{% endif %}>
{# <td>{{q['name']}}</td> #} <td>{{q['name']}}</td>
{# <td>{{q['available_number']}} left</td> #} <td>{{q['available_number']}} left</td>
{# </tr> #} </tr>
{# {% endfor %} #} {% endfor %}
{# </table> #} </table>
</details> </details>

View File

@ -29,36 +29,21 @@
<table> <table>
<tr> <tr>
<td>Room type</td> <td>Room type</td>
{# <td><strong>{{[None,'Single','Double','Triple','Quadruple','Quintuple'][len(room_members)]}} Room</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> </tr>
{# <tr> #}
{# <td>Rooms left of this type</td> #}
{# <td><strong>{{quota.get_left(len(room_members))}}</strong></td> #}
{# </tr> #}
</table> </table>
<footer> <footer>
<a href="javascript:document.getElementById('modal-roomconfirm').removeAttribute('open')" role="button">Close</a> <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> </footer>
</article> </article>
</dialog> </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 %} {% endif %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Furizon 2024 Carpooling{% endblock %} {% block title %}Furizon 2023 Carpooling{% endblock %}
{% block main %} {% block main %}
<main class="container"> <main class="container">
<header> <header>
@ -42,13 +42,13 @@
Day of departure Day of departure
<select name="day_departure" id="day_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="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="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> </select>
</label> </label>
<textarea id="message" name="message" style="height:10em;" placeholder="Write here your message" required>{{order.carpooling_message.message}}</textarea> <textarea id="message" name="message" style="height:10em;" placeholder="Write here your message" required>{{order.carpooling_message.message}}</textarea>

View File

@ -1,13 +0,0 @@
<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

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

View File

@ -1,27 +0,0 @@
{% 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" %} {% extends "base.html" %}
{% block title %}Furizon 2024 Karaoke Admin{% endblock %} {% block title %}Furizon 2023 Karaoke Admin{% endblock %}
{% block main %} {% block main %}
<main class="container"> <main class="container">
<h1>Karaoke Admin</h1> <h1>Karaoke Admin</h1>

View File

@ -1,99 +1,50 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Furizon 2024 Nosecount{% endblock %} {% block title %}Furizon 2023 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 %} {% block main %}
<main class="container"> <main class="container">
{% if order and order.isAdmin() %}
<script src="/res/scripts/roomManager.js"></script>
{% endif %}
<header> <header>
<picture> <picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)"> <source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;"> <img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
</picture> </picture>
</header> </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> <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 filtered and order %} {% if o.code == o.room_id and o.room_confirmed %}
{% for person in filtered.values() %} <h4 style="margin-top:1em;">{{o.room_name}}</h4>
{% 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;"> <div class="grid people" style="padding-bottom:1em;">
{% for m in o.room_members %} {% for m in o.room_members %}
{% if m in orders %} {% if m in orders %}
{% with person = orders[m] %} {% with person = orders[m] %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %} <div class="propic-container">
{% include 'blocks/propic.html' %} <img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
{% endwith %} <img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
</div>
<h5>{{person.ans('fursona_name')}}</h5> <h5>{{person.ans('fursona_name')}}</h5>
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed) %} {% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed and len(o.room_members) > 1) %}
{% if loop.first %} {% if loop.first %}
<hr /> <hr />
<h1>Unconfirmed rooms{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1> <h1>Unconfirmed rooms</h1>
<p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p> <p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p>
{% endif %} {% endif %}
<h4> <h3>{{o.room_name}}</h3>
<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;"> <div class="grid people" style="padding-bottom:1em;">
{% for m in o.room_members %} {% for m in o.room_members %}
{% if m in orders %} {% if m in orders %}
{% with person = orders[m] %} {% with person = orders[m] %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %} <div class="propic-container">
{% include 'blocks/propic.html' %} <img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
{% endwith %} <img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
</div>
<h5>{{person.ans('fursona_name')}}</h5> <h5>{{person.ans('fursona_name')}}</h5>
</div> </div>
{% endwith %} {% endwith %}
@ -103,54 +54,22 @@
{% if loop.last %}</div>{% endif %} {% if loop.last %}</div>{% endif %}
{% endfor %} {% endfor %}
{% for person in orders.values() if not person.room_id and (not person.room_confirmed) and not person.daily %} {% for person in orders.values() if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed)%}
{% if loop.first %} {% if loop.first %}
<hr /> <hr />
<h1>Roomless furs{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1> <h1>Roomless furs</h1>
<p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p> <p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p>
<div class="grid people" style="padding-bottom:1em;"> <div class="grid people" style="padding-bottom:1em;">
{% endif %} {% endif %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %} <div class="propic-container">
{% include 'blocks/propic.html' %} <img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
{% endwith %} <img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
</div>
<h5>{{person.ans('fursona_name')}}</h5> <h5>{{person.ans('fursona_name')}}</h5>
</div> </div>
{% if loop.last %}</div>{% endif %} {% 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> </main>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Furizon 2024 Nosecount{% endblock %} {% block title %}Furizon 2023 Nosecount{% endblock %}
{% block main %} {% block main %}
<main class="container"> <main class="container">
<header> <header>
@ -8,7 +8,7 @@
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;"> <img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
</picture> </picture>
</header> </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> <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> <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,6 +28,7 @@
<li>First three octets of your ip address</li> <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> <li>List of status codes (404, 500, 403...) you encountered, and when you encountered them</li>
</ul> </ul>
<p>This info is collected through our first-party domain <em>y.foxo.me</em></p>
<h2>Backups and export of data</h2> <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> <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>

View File

@ -1,52 +0,0 @@
{% 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 %}

View File

@ -1,24 +0,0 @@
{% 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,16 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{order.name}}'s Booking{% endblock %} {% 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 %} {% block main %}
{% set locale = order.get_language() %}
<main class="container"> <main class="container">
<header> <header>
<picture> <picture>
@ -22,7 +12,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>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>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 Overlord!</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>
<hr /> <hr />
<h2 id="info">Useful information</h2> <h2 id="info">Useful information</h2>
<table> <table>
@ -34,14 +24,13 @@
</tr> </tr>
<tr> <tr>
<th>When{{' (convention)' if order.has_early or order.has_late else ''}}?</th> <th>When{{' (convention)' if order.has_early or order.has_late else ''}}?</th>
{# This should be early/late excluded! #} <td>13 October → 15 October 2023</td></td>
<td><img src="/res/icons/calendar.svg" class="icon" />4 June → 8 June 2024</td></td>
</tr> </tr>
{% if order.has_early or order.has_late %} {% if order.has_early or order.has_late %}
<tr> <tr>
<th>When (check-in)?</th> <th>When (check-in)?</th>
<td> <td>
{{('3' if order.has_early else '4')|safe}} June → {{('9' if order.has_late else '8')|safe}} June 2024 {{('12' if order.has_early else '13')|safe}} October → {{('16' if order.has_late else '15')|safe}} October 2023
{% if order.has_early %} {% if order.has_early %}
<span class="tag">EARLY</span> <span class="tag">EARLY</span>
{% endif %} {% endif %}
@ -64,36 +53,19 @@
{% if order.status == 'paid' and order.room_confirmed %} {% if order.status == 'paid' and order.room_confirmed %}
<br /> <br />
<img src="/res/icons/pdf.svg" class="icon" /> <img src="/res/icons/pdf.svg" class="icon" />
<a href="/manage/download_ticket?name=OVERLORD-{{order.code}}.pdf" target="_blank">Download ticket PDF</a> <a href="/manage/download_ticket?name=RIVERSIDE-{{order.code}}.pdf" target="_blank">Download ticket PDF</a>
{% endif %} {% endif %}
</td> </td>
</tr> </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> </table>
<h2>Manage your booking</h2> <h2>Manage your booking</h2>
{% include 'blocks/payment.html' %} {% include 'blocks/payment.html' %}
{% if not order.daily %} {% if order.position_positiontypeid not in ITEM_IDS['daily'] %}
{% include 'blocks/room.html' %} {% include 'blocks/room.html' %}
{% endif %} {% endif %}
{% include 'blocks/badge.html' %} {% 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"> <details id="barcard">
<summary role="button"><img src="/res/icons/bar.svg" class="icon" />Barcard</summary> <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> <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>

View File

@ -1,88 +0,0 @@
{% 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 ''}} - {{room_order.room_person_no}} People max</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 %}

352
utils.py
View File

@ -1,352 +0,0 @@
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")
# Returns true if the logged used is an admin OR if it's an admin logged as another user
async def isSessionAdmin(request, order):
if(order.isAdmin()): return True
orgCode = request.cookies.get("foxo_code_ORG")
orgSecret = request.cookies.get("foxo_secret_ORG")
if orgCode != None and orgSecret != None:
user = await request.app.ctx.om.get_order(code=orgCode)
if(user == None): return False
if(user.secret != orgSecret): raise exceptions.Forbidden("Birichino :)")
return user.isAdmin()
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)