Compare commits
109 Commits
Author | SHA1 | Date |
---|---|---|
Andrea | 77c102c1cf | |
Andrea | e34af503ff | |
Andrea | f3a76b6e93 | |
Andrea | a0bcf3c046 | |
Andrea | d9a792aef0 | |
Andrea | 41373dfcfa | |
Andrea Carulli | 13e5217601 | |
Andrea | b968a78aa5 | |
Andrea Carulli | 11fd547250 | |
Andrea | 63c0bb75db | |
drew | ab2c8d4b85 | |
Stranck | df4f2eaf81 | |
drew | cd2a5405db | |
Stranck | 84bc070593 | |
Stranck | 1e6b400b2c | |
Stranck | 383b5bbede | |
Stranck | 938bc68383 | |
Stranck | 0d6789d307 | |
drew | db45c2e7e3 | |
Stranck | bc366c85e5 | |
drew | 687eaf7317 | |
Luca Sorace "Stranck | d974870963 | |
Luca Sorace "Stranck | f4ebc83a9b | |
drew | 76f0eee841 | |
stranck | c405a49366 | |
Andrea | cb3a501280 | |
drew | edc306c6c1 | |
Luca Sorace "Stranck | b1a67e74e3 | |
Stranck | f233fb0b68 | |
Stranck | 99b8c87e5c | |
Stranck | 9f191c3dec | |
Stranck | c6bc9c65ac | |
Stranck | 5b66a58399 | |
Luca Sorace "Stranck | 3919d512a1 | |
Stranck | 5d79052e59 | |
Stranck | 1d7ea375c1 | |
Stranck | 968f5f09ed | |
Stranck | 4e1120a93f | |
stranck | 44b534b283 | |
Andrea | f8e58fd35d | |
Andrea | ac640c4185 | |
drew | 758cf4e3a6 | |
Luca Sorace "Stranck | 8d314d2a63 | |
Luca Sorace "Stranck | 61447ee768 | |
Luca Sorace "Stranck | cfefd44435 | |
Luca Sorace "Stranck | f59c288908 | |
drew | 78f677d19d | |
Luca Sorace "Stranck | 878719260d | |
Luca Sorace "Stranck | 7574a9d356 | |
Luca Sorace "Stranck | 165c9d6bb5 | |
Luca Sorace "Stranck | 0d663c5b58 | |
Luca Sorace "Stranck | 6494a06ca8 | |
Luca Sorace "Stranck | 951e4c0015 | |
Luca Sorace "Stranck | 5d851acbeb | |
stranck | e780a2a0c0 | |
Andrea | 1da054c9ff | |
Andrea | a2b9f7a7c1 | |
Andrea Carulli | a154c3059b | |
Andrea | 16ae51fb08 | |
Andrea Carulli | aa54032492 | |
Andrea | 6fe87b6ea1 | |
Andrea Carulli | 123a1dc3c5 | |
Andrea | 7299bdb840 | |
drew | 3878076e0f | |
Stranck | aed8bf41b7 | |
stranck | 59c2a7d926 | |
Andrea | 2662ece8ab | |
drew | c9a7e5da8a | |
stranck | c5d685b42b | |
Andrea | eabfc2d5b0 | |
drew | 957ebb5899 | |
Stranck | 8b07fa55b7 | |
Stranck | ca2ad6589b | |
Stranck | c82d075913 | |
Stranck | 0af0849f13 | |
Stranck | 274dcbb3a3 | |
Stranck | f3eb905298 | |
Stranck | dafe98dfbb | |
Stranck | 456361c585 | |
Stranck | c5fb4fd55d | |
Stranck | 9dfacc3b5b | |
Stranck | 04e7bd3005 | |
Stranck | 5c44c620e4 | |
Stranck | 5d815defc8 | |
stranck | 95525d28a2 | |
Andrea | ffda843d45 | |
drew | 8496c89977 | |
Stranck | d08cded007 | |
stranck | 76c8df9e03 | |
Stranck | c7159cf3b4 | |
Stranck | fd3b0e727c | |
Stranck | bebc7011cf | |
Andrea | 823aaf4f13 | |
stranck | f8ed348d51 | |
Andrea | 53232de597 | |
drew | 01da35560f | |
Stranck | 25ed127e81 | |
Stranck | 6b70d91201 | |
Stranck | 866ac5b9d5 | |
stranck | ee25571cd7 | |
Andrea | 32e78319fc | |
drew | e65a37826c | |
Stranck | fee70fac7c | |
stranck | 3482c02fc5 | |
Stranck | 7246973651 | |
Andrea | a70f727009 | |
drew | 05eb2172ce | |
Stranck | 3a507b89ac | |
Stranck | 160d186aeb |
|
@ -154,6 +154,8 @@ cython_debug/
|
|||
|
||||
res/propic/propic_*
|
||||
|
||||
res/rooms/*
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
|
@ -161,3 +163,10 @@ res/propic/propic_*
|
|||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
config.py
|
||||
furizon_webinit_riverside2023.tar.gz
|
||||
diomerdas
|
||||
furizon.net/site/*
|
||||
furizon.net.zip
|
||||
stuff/secrets.py
|
||||
backups/*
|
||||
log.txt
|
||||
|
|
26
README.md
|
@ -0,0 +1,26 @@
|
|||
# Furizon Webint
|
||||
Furizon Webint is a powerful control panel designed to complement Pretix, providing management of various aspects related to the attendance of participants at furry conventions. Originally developed for Furizon Beyond (2023), this application is currently undergoing a rehaul to become more versatile and adaptable for use in any convention.
|
||||
|
||||
## How does it work?
|
||||
The integration with Pretix is achieved by leveraging a simple nginx rule. When individuals place orders through Pretix, they usually receive a "magic" link that allows them to manage their order. Using a nginx rule, we redirect these requests to this backend. This process is seamless because the essential information needed for managing Pretix orders can still be accessed via a shorter URL without compromising any functionality.
|
||||
|
||||
## Why not a pretix plugin?
|
||||
Developing plugins for Pretix was far too tedious, and Pretix didn't have the flexibility needed for this panel.
|
||||
|
||||
## What can it do?
|
||||
- User badges management (allow attendees to upload pictures within the deadlines)
|
||||
- Manage hotel rooms (attendees can create, join, delete rooms)
|
||||
- Show a nosecount public page
|
||||
- Data export
|
||||
- Car pooling (let attendees post announcements and organize trips)
|
||||
- Karaoke Queue management (apply to sing for the karaoke contest and manage the queue)
|
||||
- Manage the events and present them via API for usage with he app
|
||||
- Export an API to be used for the mobile app (no plans to open source that, sorry ☹️)
|
||||
- Check-in management
|
||||
|
||||
## How to run it
|
||||
1. Create a Python virtual environment (venv).
|
||||
2. Install the required dependencies from the `requirements.txt` file.
|
||||
3. Edit the `config.py` file with your specific data. You can use `config.example.py` as a template to guide you.
|
||||
4. Set up an nginx rule to redirect requests for `/manage/` and `/[a-z0-9]+/[a-z0-9]+/order/[A-Z0-9]+/[a-z0-9]+/open/[a-z0-9]+/` to the Furizon Webint backend.
|
||||
5. Run `app.py`. By default, the application will listen on `0.0.0.0:8188`.
|
|
@ -0,0 +1,370 @@
|
|||
from sanic import response, redirect, Blueprint, exceptions
|
||||
from email_util import send_missing_propic_message
|
||||
from random import choice
|
||||
from room import unconfirm_room_by_order
|
||||
from config import *
|
||||
from utils import *
|
||||
from ext import *
|
||||
from io import StringIO
|
||||
from sanic.log import logger
|
||||
import csv
|
||||
import time
|
||||
import json
|
||||
import math
|
||||
|
||||
bp = Blueprint("admin", url_prefix="/manage/admin")
|
||||
|
||||
@bp.middleware
|
||||
async def credentials_check(request: Request):
|
||||
order = await get_order(request)
|
||||
if not order:
|
||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
if EXTRA_PRINTS:
|
||||
logger.info(f"Checking admin credentials of {order.code} with secret {order.secret}")
|
||||
if not order.isAdmin() : raise exceptions.Forbidden("Birichino :)")
|
||||
|
||||
|
||||
|
||||
@bp.get('/cache/clear')
|
||||
async def clear_cache(request, order:Order):
|
||||
success = await request.app.ctx.om.fill_cache()
|
||||
if not success: raise exceptions.ServerError("An error occurred while loading the cache")
|
||||
return redirect(f'/manage/admin')
|
||||
|
||||
@bp.get('/loginas/<code>')
|
||||
async def login_as(request, code, order:Order):
|
||||
dOrder = await get_order_by_code(request, code, throwException=True)
|
||||
if(dOrder.isAdmin()):
|
||||
raise exceptions.Forbidden("You can't login as another admin!")
|
||||
|
||||
if EXTRA_PRINTS:
|
||||
logger.info(f"Swapping login: {order.secret} {order.code} -> {dOrder.secret} {code}")
|
||||
r = redirect(f'/manage/welcome')
|
||||
r.cookies['foxo_code_ORG'] = order.code
|
||||
r.cookies['foxo_secret_ORG'] = order.secret
|
||||
r.cookies['foxo_code'] = code
|
||||
r.cookies['foxo_secret'] = dOrder.secret
|
||||
return r
|
||||
|
||||
@bp.get('/room/verify')
|
||||
async def verify_rooms(request, order:Order):
|
||||
await clear_cache(request, order)
|
||||
already_checked, success = await request.app.ctx.om.update_cache()
|
||||
if not already_checked and success:
|
||||
orders = filter(lambda x: x.status not in ['c', 'e'] and x.room_id == x.code, request.app.ctx.om.cache.values())
|
||||
await validate_rooms(request, orders, None)
|
||||
return redirect(f'/manage/admin')
|
||||
|
||||
@bp.get('/room/unconfirm/<code>')
|
||||
async def unconfirm_room(request, code, order:Order):
|
||||
dOrder = await get_order_by_code(request, code, throwException=True)
|
||||
await unconfirm_room_by_order(order=dOrder, throw=True, request=request)
|
||||
return redirect(f'/manage/nosecount')
|
||||
|
||||
@bp.get('/room/autoconfirm')
|
||||
async def autoconfirm_room(request, code, order:Order):
|
||||
await clear_cache(request, order)
|
||||
orders = request.app.ctx.om.cache.values()
|
||||
for order in orders:
|
||||
if(order.code == order.room_id and not order.room_confirmed and len(order.room_members) == order.room_person_no):
|
||||
logger.info(f"Auto-Confirming room {order.room_id}")
|
||||
await confirm_room_by_order(order, request)
|
||||
return redirect(f'/manage/admin')
|
||||
|
||||
@bp.get('/room/delete/<code>')
|
||||
async def delete_room(request, code, order:Order):
|
||||
await clear_cache(request, order)
|
||||
dOrder = await get_order_by_code(request, code, throwException=True)
|
||||
|
||||
ppl = await get_people_in_room_by_code(request, code)
|
||||
for p in ppl:
|
||||
await p.edit_answer('room_id', None)
|
||||
await p.edit_answer('room_confirmed', "False")
|
||||
await p.edit_answer('room_name', None)
|
||||
await p.edit_answer('pending_room', None)
|
||||
await p.edit_answer('pending_roommates', None)
|
||||
await p.edit_answer('room_members', None)
|
||||
await p.edit_answer('room_owner', None)
|
||||
await p.edit_answer('room_secret', None)
|
||||
await p.send_answers()
|
||||
|
||||
await dOrder.send_answers()
|
||||
return redirect(f'/manage/nosecount')
|
||||
|
||||
@bp.post('/room/rename/<code>')
|
||||
async def rename_room(request, code, order:Order):
|
||||
await clear_cache(request, order)
|
||||
dOrder = await get_order_by_code(request, code, throwException=True)
|
||||
|
||||
name = request.form.get('name')
|
||||
if len(name) > 64 or len(name) < 4:
|
||||
raise exceptions.BadRequest("Your room name is invalid. Please try another one.")
|
||||
|
||||
await dOrder.edit_answer("room_name", name)
|
||||
await dOrder.send_answers()
|
||||
return redirect(f'/manage/nosecount')
|
||||
|
||||
@bp.get('/room/wizard')
|
||||
async def room_wizard(request, order:Order):
|
||||
'''Tries to autofill unconfirmed rooms and other matches together'''
|
||||
# Clear cache first
|
||||
await clear_cache(request, order)
|
||||
|
||||
#Separate orders which have incomplete rooms and which have no rooms
|
||||
all_orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e'] and not value.daily}
|
||||
orders = {key:value for key,value in sorted(all_orders.items(), key=lambda x: x[1].ans('fursona_name')) if not value.room_confirmed}
|
||||
# Orders with incomplete rooms
|
||||
incomplete_orders = {key:value for key,value in orders.items() if value.code == value.room_id and (value.room_person_no - len(value.room_members)) > 0}
|
||||
# Roomless furs
|
||||
roomless_orders = {key:value for key,value in orders.items() if not value.room_id and not value.daily}
|
||||
|
||||
# Result map
|
||||
result_map = {}
|
||||
|
||||
# Check overflows
|
||||
room_quota_overflow = {}
|
||||
for key, value in ITEM_VARIATIONS_MAP['bed_in_room'].items():
|
||||
room_quota = get_quota(ITEMS_ID_MAP['bed_in_room'], value)
|
||||
capacity = ROOM_CAPACITY_MAP[key] if key in ROOM_CAPACITY_MAP else 1
|
||||
current_quota = len(list(filter(lambda y: y.bed_in_room == value and y.room_owner == True, orders.values())))
|
||||
room_quota_overflow[value] = current_quota - int(room_quota.size / capacity) if room_quota else 0
|
||||
|
||||
# Init rooms to remove
|
||||
result_map["void"] = []
|
||||
|
||||
# Remove rooms that are over quota
|
||||
for room_type, overflow_qty in {key:value for key,value in room_quota_overflow.items() if value > 0}.items():
|
||||
sorted_rooms = sorted(incomplete_orders.values(), key=lambda r: len(r.room_members))
|
||||
for room_to_remove in sorted_rooms[:overflow_qty]:
|
||||
# Room codes to remove
|
||||
result_map["void"].append(room_to_remove.code)
|
||||
# Move room members to the roomless list
|
||||
for member_code in room_to_remove.room_members:
|
||||
roomless_orders[member_code] = all_orders[member_code]
|
||||
del incomplete_orders[room_to_remove.code]
|
||||
|
||||
# Fill already existing rooms
|
||||
for room_order in incomplete_orders.items():
|
||||
room = room_order[1]
|
||||
to_add = []
|
||||
missing_slots = room.room_person_no - len(room.room_members)
|
||||
for i in range(missing_slots):
|
||||
compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room}
|
||||
if len(compatible_roomates.items()) == 0: break
|
||||
# Try picking a roomate that's from the same country and room type
|
||||
country = room.country.lower()
|
||||
roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country}
|
||||
if len(roomless_by_country.items()) > 0:
|
||||
code_to_add = list(roomless_by_country.keys())[0]
|
||||
to_add.append(code_to_add)
|
||||
del roomless_orders[code_to_add]
|
||||
else:
|
||||
# If not, add first roomless there is
|
||||
code_to_add = list(compatible_roomates.keys())[0]
|
||||
to_add.append(code_to_add)
|
||||
del roomless_orders[code_to_add]
|
||||
result_map[room.code] = {
|
||||
'type': 'add_existing',
|
||||
'to_add': to_add
|
||||
}
|
||||
|
||||
generated_counter = 0
|
||||
# Create additional rooms
|
||||
while len(roomless_orders.items()) > 0:
|
||||
room = list(roomless_orders.items())[0][1]
|
||||
to_add = []
|
||||
missing_slots = room.room_person_no - len(room.room_members)
|
||||
for i in range(missing_slots):
|
||||
compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room}
|
||||
if len(compatible_roomates.items()) == 0: break
|
||||
# Try picking a roomate that's from the same country and room type
|
||||
country = room.country.lower()
|
||||
roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country}
|
||||
if len(roomless_by_country.items()) > 0:
|
||||
code_to_add = list(roomless_by_country.keys())[0]
|
||||
to_add.append(code_to_add)
|
||||
del roomless_orders[code_to_add]
|
||||
else:
|
||||
# If not, add first roomless there is
|
||||
code_to_add = list(compatible_roomates.keys())[0]
|
||||
to_add.append(code_to_add)
|
||||
del roomless_orders[code_to_add]
|
||||
generated_counter += 1
|
||||
result_map[room.code] = {
|
||||
'type': 'new',
|
||||
'room_name': f'Generated Room {generated_counter}',
|
||||
'room_type': room.bed_in_room,
|
||||
'to_add': to_add
|
||||
}
|
||||
|
||||
result_map["infinite"] = { 'to_add': [] }
|
||||
tpl = request.app.ctx.tpl.get_template('wizard.html')
|
||||
return html(tpl.render(order=order, all_orders=all_orders, unconfirmed_orders=orders, data=result_map, jsondata=json.dumps(result_map, skipkeys=True, ensure_ascii=False)))
|
||||
|
||||
@bp.post('/room/wizard/submit')
|
||||
async def submit_from_room_wizard(request:Request, order:Order):
|
||||
'''Will apply changes to the rooms'''
|
||||
await request.app.ctx.om.fill_cache()
|
||||
|
||||
data = json.loads(request.body)
|
||||
|
||||
# Phase 1 - Delete all rooms in void
|
||||
if 'void' in data:
|
||||
for room_code in data['void']:
|
||||
ppl = await get_people_in_room_by_code(request, room_code)
|
||||
for p in ppl:
|
||||
await p.edit_answer('room_id', None)
|
||||
await p.edit_answer('room_confirmed', "False")
|
||||
await p.edit_answer('room_name', None)
|
||||
await p.edit_answer('pending_room', None)
|
||||
await p.edit_answer('pending_roommates', None)
|
||||
await p.edit_answer('room_members', None)
|
||||
await p.edit_answer('room_owner', None)
|
||||
await p.edit_answer('room_secret', None)
|
||||
await p.send_answers()
|
||||
logger.info(f"Deleted rooms {', '.join(data['void'])}")
|
||||
|
||||
# Phase 2 - Join roomless to other rooms or add new rooms
|
||||
for room_code, value in {key:value for key,value in data.items() if key.lower() not in ['void', 'infinite']}.items():
|
||||
if not value['to_add'] or len(value['to_add']) == 0: continue
|
||||
room_order = await request.app.ctx.om.get_order(code=room_code)
|
||||
# Preconditions
|
||||
if not room_order: raise exceptions.BadRequest(f"Order {room_code} does not exist.")
|
||||
if room_order.daily == True: raise exceptions.BadRequest(f"Order {room_code} is daily.")
|
||||
if room_order.status != 'paid': raise exceptions.BadRequest(f"Order {room_code} hasn't paid.")
|
||||
if room_order.room_owner:
|
||||
if room_order.room_person_no < len(room_order.room_members) + (len(value['to_add']) if value['to_add'] else 0):
|
||||
raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.")
|
||||
elif room_order.room_person_no < (len(value['to_add']) if value['to_add'] else 0):
|
||||
raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.")
|
||||
|
||||
# Adding roomless orders to existing rooms
|
||||
if value['type'] == 'add_existing' or value['type'] == 'new':
|
||||
if value['type'] == 'new':
|
||||
if room_order.room_owner: exceptions.BadRequest(f"Order {room_code} is already a room owner.")
|
||||
# Create room data
|
||||
await room_order.edit_answer('room_name', value['room_name'])
|
||||
await room_order.edit_answer('room_id', room_order.code)
|
||||
await room_order.edit_answer('room_secret', ''.join(choice('0123456789') for _ in range(6)))
|
||||
elif not room_order.room_owner:
|
||||
raise exceptions.BadRequest(f"Order {room_code} is not a room owner.")
|
||||
# Add members
|
||||
for new_member_code in value['to_add']:
|
||||
pending_member = await request.app.ctx.om.get_order(code=new_member_code)
|
||||
# Preconditions
|
||||
if pending_member.daily == True: raise exceptions.BadRequest(f"Order {pending_member.code} is daily.")
|
||||
if pending_member.status != 'paid': raise exceptions.BadRequest(f"Order {new_member_code} hasn't paid.")
|
||||
if pending_member.bed_in_room != room_order.bed_in_room: raise exceptions.BadRequest(f"Order {new_member_code} has a different room type than {room_code}.")
|
||||
if pending_member.room_owner: exceptions.BadRequest(f"Order {new_member_code} is already a room owner.")
|
||||
if pending_member.room_id and pending_member.room_id not in data['void']: exceptions.BadRequest(f"Order {new_member_code} is in another room.")
|
||||
await pending_member.edit_answer('room_id', room_order.code)
|
||||
await pending_member.edit_answer('room_confirmed', "True")
|
||||
await pending_member.edit_answer('pending_room', None)
|
||||
await pending_member.send_answers()
|
||||
logger.info(f"{'Created' if value['type'] == 'new' else 'Edited'} {str(room_order)}")
|
||||
# Confirm members that were already inside the room
|
||||
if value['type'] == 'add_existing':
|
||||
for already_member in list(filter(lambda rm: rm.code in room_order.room_members and rm.code != room_order.code, request.app.ctx.om.cache.values())):
|
||||
await already_member.edit_answer('room_confirmed', "True")
|
||||
await already_member.send_answers()
|
||||
else: raise exceptions.BadRequest(f"Unexpected type ({value['type']})")
|
||||
await room_order.edit_answer('pending_room', None)
|
||||
await room_order.edit_answer('pending_roommates', None)
|
||||
await room_order.edit_answer('room_confirmed', "True")
|
||||
await room_order.edit_answer('room_members', ','.join(list(set([*room_order.room_members, room_order.code, *value['to_add']]))))
|
||||
await room_order.send_answers()
|
||||
await request.app.ctx.om.fill_cache()
|
||||
return text('done', status=200)
|
||||
|
||||
|
||||
@bp.get('/propic/remind')
|
||||
async def propic_remind_missing(request, order:Order):
|
||||
await clear_cache(request, order)
|
||||
|
||||
orders = request.app.ctx.om.cache.values()
|
||||
order: Order
|
||||
for order in orders:
|
||||
missingPropic = order.propic is None
|
||||
missingFursuitPropic = order.is_fursuiter and order.propic_fursuiter is None
|
||||
if(missingPropic or missingFursuitPropic):
|
||||
# print(f"{order.code}: prp={missingPropic} fpr={missingFursuitPropic} - {order.name}")
|
||||
await send_missing_propic_message(order, missingPropic, missingFursuitPropic)
|
||||
|
||||
return redirect(f'/manage/admin')
|
||||
|
||||
@bp.get('/export/export')
|
||||
async def export_export(request, order:Order):
|
||||
await clear_cache(request, order)
|
||||
|
||||
data = StringIO()
|
||||
w = csv.writer(data)
|
||||
|
||||
w.writerow(['Status', 'Code', 'First name', 'Last name', 'Nick', 'State', 'Card', 'Artist', 'Fursuiter', 'Sponsorhip', 'Early', 'Late', 'Daily', 'Daily days', 'Shirt', 'Room type', 'Room count', 'Room members', 'Payment', 'Price', 'Refunds', 'Staff'])
|
||||
|
||||
orders = request.app.ctx.om.cache.values()
|
||||
order: Order
|
||||
for order in orders:
|
||||
w.writerow([
|
||||
order.status,
|
||||
order.code,
|
||||
order.first_name,
|
||||
order.last_name,
|
||||
order.name,
|
||||
order.country,
|
||||
order.has_card,
|
||||
order.is_artist,
|
||||
order.is_fursuiter,
|
||||
order.sponsorship,
|
||||
order.has_early,
|
||||
order.has_late,
|
||||
order.daily,
|
||||
','.join(order.dailyDays),
|
||||
order.shirt_size,
|
||||
ROOM_TYPE_NAMES[order.bed_in_room] if order.bed_in_room in ROOM_TYPE_NAMES else "-",
|
||||
len(order.room_members),
|
||||
','.join(order.room_members),
|
||||
order.payment_provider,
|
||||
order.total - order.fees,
|
||||
order.refunds,
|
||||
order.ans('staff_role') or 'attendee',
|
||||
])
|
||||
|
||||
data.seek(0)
|
||||
str = data.read() #data.read().decode("UTF-8")
|
||||
data.flush()
|
||||
data.close()
|
||||
|
||||
return raw(str, status=200, headers={'Content-Disposition': f'attachment; filename="export_{int(time.time())}.csv"', "Content-Type": "text/csv; charset=UTF-8"})
|
||||
|
||||
@bp.get('/export/hotel')
|
||||
async def export_hotel(request, order:Order):
|
||||
await clear_cache(request, order)
|
||||
|
||||
data = StringIO()
|
||||
w = csv.writer(data)
|
||||
|
||||
w.writerow(['Room type', 'Room name', 'Room code', 'First name', 'Last name', 'Birthday', 'Address', 'Email', 'Phone number', 'Status'])
|
||||
|
||||
orders = sorted(request.app.ctx.om.cache.values(), key=lambda d: (d.room_id if d.room_id != None else "~"))
|
||||
order: Order
|
||||
for order in orders:
|
||||
w.writerow([
|
||||
ROOM_TYPE_NAMES[order.bed_in_room] if order.bed_in_room in ROOM_TYPE_NAMES else "-",
|
||||
order.room_name,
|
||||
order.room_id,
|
||||
order.first_name,
|
||||
order.last_name,
|
||||
order.birth_date,
|
||||
order.address,
|
||||
order.email,
|
||||
order.phone,
|
||||
order.status,
|
||||
order.code
|
||||
])
|
||||
|
||||
data.seek(0)
|
||||
str = data.read() #data.read().decode("UTF-8")
|
||||
data.flush()
|
||||
data.close()
|
||||
|
||||
return raw(str, status=200, headers={'Content-Disposition': f'attachment; filename="hotel_{int(time.time())}.csv"', "Content-Type": "text/csv; charset=UTF-8"})
|
19
api.py
|
@ -9,6 +9,9 @@ import random
|
|||
import string
|
||||
import httpx
|
||||
import json
|
||||
import traceback
|
||||
from sanic.log import logger
|
||||
from email_util import send_app_login_attempt
|
||||
|
||||
bp = Blueprint("api", url_prefix="/manage/api")
|
||||
|
||||
|
@ -185,7 +188,8 @@ async def nfc_scan(request, nfc_id):
|
|||
@bp.get("/get_token/<code>/<login_code>")
|
||||
async def get_token_from_code(request, code, login_code):
|
||||
if not code in request.app.ctx.login_codes:
|
||||
print(request.app.ctx.login_codes)
|
||||
if DEV_MODE and EXTRA_PRINTS:
|
||||
logger.debug(request.app.ctx.login_codes)
|
||||
return response.json({'ok': False, 'error': 'You need to reauthenticate. The code has expired.'}, status=401)
|
||||
|
||||
if request.app.ctx.login_codes[code][1] == 0:
|
||||
|
@ -219,16 +223,9 @@ async def get_token(request, code):
|
|||
request.app.ctx.login_codes[code] = [''.join(random.choice(string.digits) for _ in range(6)), 3]
|
||||
|
||||
try:
|
||||
msg = MIMEText(f"Hello {user.name}!\n\nWe have received a request to login in the app. If you didn't do this, please ignore this email. Somebody is probably playing with you.\n\nYour login code is: {request.app.ctx.login_codes[code][0]}\n\nPlease do not tell this to anybody!")
|
||||
msg['Subject'] = '[Furizon] Your login code'
|
||||
msg['From'] = 'Furizon <no-reply@furizon.net>'
|
||||
msg['To'] = f"{user.name} <{user.email}>"
|
||||
|
||||
s = smtplib.SMTP_SSL(SMTP_HOST)
|
||||
s.login(SMTP_USER, SMTP_PASSWORD)
|
||||
s.sendmail(msg['From'], msg['to'], msg.as_string())
|
||||
s.quit()
|
||||
except:
|
||||
await send_app_login_attempt(user, request.app.ctx.login_codes[code][0])
|
||||
except Exception:
|
||||
logger.error(f"[API] [GET_TOKEN] Error while sending email.\n{traceback.format_exc()}")
|
||||
return response.json({'ok': False, 'error': 'There has been an issue sending your e-mail. Please try again later or report to an admin.'}, status=500)
|
||||
|
||||
return response.json({'ok': True, 'message': 'A login code has been sent to your email.'})
|
||||
|
|
172
app.py
|
@ -1,20 +1,24 @@
|
|||
from sanic import Sanic, response, exceptions
|
||||
from sanic.response import text, html, redirect, raw
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from time import time
|
||||
from time import time, sleep
|
||||
import httpx
|
||||
import re
|
||||
import json
|
||||
import logging
|
||||
from os.path import join
|
||||
from ext import *
|
||||
from config import *
|
||||
from aztec_code_generator import AztecCode
|
||||
from propic import resetDefaultPropic
|
||||
from io import BytesIO
|
||||
from asyncio import Queue
|
||||
from messages import LOCALES
|
||||
import sqlite3
|
||||
|
||||
log = logging.getLogger()
|
||||
import requests
|
||||
import sys
|
||||
from sanic.log import logger, logging, access_logger
|
||||
from metrics import *
|
||||
from email_util import killSmptClient
|
||||
import pretixClient
|
||||
import traceback
|
||||
|
||||
app = Sanic(__name__)
|
||||
app.static("/res", "res/")
|
||||
|
@ -25,33 +29,49 @@ app.ext.add_dependency(Quotas, get_quotas)
|
|||
from room import bp as room_bp
|
||||
from propic import bp as propic_bp
|
||||
from karaoke import bp as karaoke_bp
|
||||
from export import bp as export_bp
|
||||
from stats import bp as stats_bp
|
||||
from api import bp as api_bp
|
||||
from carpooling import bp as carpooling_bp
|
||||
from checkin import bp as checkin_bp
|
||||
from admin import bp as admin_bp
|
||||
|
||||
app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp])
|
||||
app.blueprint([room_bp, karaoke_bp, propic_bp, stats_bp, api_bp, carpooling_bp, checkin_bp, admin_bp])
|
||||
|
||||
@app.exception(exceptions.SanicException)
|
||||
async def clear_session(request, exception):
|
||||
tpl = app.ctx.tpl.get_template('error.html')
|
||||
r = html(tpl.render(exception=exception))
|
||||
|
||||
if exception.status_code == 403:
|
||||
del r.cookies["foxo_code"]
|
||||
del r.cookies["foxo_secret"]
|
||||
|
||||
async def clear_session(response):
|
||||
response.delete_cookie("foxo_code")
|
||||
response.delete_cookie("foxo_secret")
|
||||
|
||||
@app.exception(exceptions.SanicException if DEV_MODE else Exception)
|
||||
async def handleException(request, exception):
|
||||
incErrorNo()
|
||||
logger.warning(f"{request} -> {exception}")
|
||||
statusCode = exception.status_code if hasattr(exception, 'status_code') else 500
|
||||
try:
|
||||
tpl = app.ctx.tpl.get_template('error.html')
|
||||
r = html(tpl.render(exception=exception, status_code=statusCode), status=statusCode)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
if statusCode == 403:
|
||||
await clear_session(r)
|
||||
return r
|
||||
|
||||
|
||||
@app.before_server_start
|
||||
async def main_start(*_):
|
||||
print(">>>>>> main_start <<<<<<")
|
||||
logger.info(f"[{app.name}] >>>>>> main_start <<<<<<")
|
||||
logger.setLevel(LOG_LEVEL)
|
||||
access_logger.addFilter(MetricsFilter())
|
||||
|
||||
app.config.REQUEST_MAX_SIZE = PROPIC_MAX_FILE_SIZE * 3
|
||||
|
||||
app.ctx.om = OrderManager()
|
||||
if FILL_CACHE:
|
||||
log.info("Filling cache!")
|
||||
await app.ctx.om.fill_cache()
|
||||
log.info("Cache fill done!")
|
||||
checked, success = await app.ctx.om.update_cache(check_itemsQuestions=True)
|
||||
if checked and not success:
|
||||
logger.error(f"[{app.name}] Failure in app startup: An error occurred while loading items or questions or cache.")
|
||||
app.stop()
|
||||
|
||||
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
|
||||
|
||||
|
@ -60,7 +80,13 @@ async def main_start(*_):
|
|||
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
|
||||
app.ctx.tpl.globals.update(time=time)
|
||||
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
|
||||
app.ctx.tpl.globals.update(ITEM_IDS=ITEM_IDS)
|
||||
app.ctx.tpl.globals.update(LOCALES=LOCALES)
|
||||
app.ctx.tpl.globals.update(ITEMS_ID_MAP=ITEMS_ID_MAP)
|
||||
app.ctx.tpl.globals.update(ITEM_VARIATIONS_MAP=ITEM_VARIATIONS_MAP)
|
||||
app.ctx.tpl.globals.update(ROOM_TYPE_NAMES=ROOM_TYPE_NAMES)
|
||||
app.ctx.tpl.globals.update(PROPIC_MIN_SIZE=PROPIC_MIN_SIZE)
|
||||
app.ctx.tpl.globals.update(PROPIC_MAX_SIZE=PROPIC_MAX_SIZE)
|
||||
app.ctx.tpl.globals.update(PROPIC_MAX_FILE_SIZE=sizeof_fmt(PROPIC_MAX_FILE_SIZE))
|
||||
app.ctx.tpl.globals.update(int=int)
|
||||
app.ctx.tpl.globals.update(len=len)
|
||||
|
||||
|
@ -72,6 +98,11 @@ async def gen_barcode(request, code):
|
|||
|
||||
return raw(img.getvalue(), content_type="image/png")
|
||||
|
||||
@app.route("/manage/lol")
|
||||
async def lol(request: Request):
|
||||
await get_quotas(request)
|
||||
return text('hi')
|
||||
|
||||
@app.route(f"/{ORGANIZER}/{EVENT_NAME}/order/<code>/<secret>/open/<secret2>")
|
||||
async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
||||
|
||||
|
@ -79,17 +110,16 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
|||
if order and order.code != code: order = None
|
||||
|
||||
if not order:
|
||||
async with httpx.AsyncClient() as client:
|
||||
res = await client.get(join(base_url, f"orders/{code}/"), headers=headers)
|
||||
print(res.json())
|
||||
if res.status_code != 200:
|
||||
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")
|
||||
|
||||
res = res.json()
|
||||
if secret != res['secret']:
|
||||
raise exceptions.Forbidden("The secret part of the url is not correct. Check your E-Mail for the correct link, or contact support!")
|
||||
r.cookies['foxo_code'] = code
|
||||
r.cookies['foxo_secret'] = secret
|
||||
res = await pretixClient.get(f"orders/{code}/", expectedStatusCodes=None)
|
||||
|
||||
if res.status_code != 200:
|
||||
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")
|
||||
|
||||
res = res.json()
|
||||
if secret != res['secret']:
|
||||
raise exceptions.Forbidden("The secret part of the url is not correct. Check your E-Mail for the correct link, or contact support!")
|
||||
r.cookies['foxo_code'] = code
|
||||
r.cookies['foxo_secret'] = secret
|
||||
return r
|
||||
|
||||
@app.route("/manage/privacy")
|
||||
|
@ -102,6 +132,11 @@ async def welcome(request, order: Order, quota: Quotas):
|
|||
|
||||
if not order:
|
||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
||||
if order.ans("propic_file") is None:
|
||||
await resetDefaultPropic(request, order, False)
|
||||
if order.ans("propic_fursuiter_file") is None:
|
||||
await resetDefaultPropic(request, order, True)
|
||||
|
||||
pending_roommates = []
|
||||
if order.pending_roommates:
|
||||
|
@ -123,10 +158,10 @@ async def welcome(request, order: Order, quota: Quotas):
|
|||
if member_id == order.code:
|
||||
room_members.append(order)
|
||||
else:
|
||||
room_members.append(await app.ctx.om.get_order(code=member_id, cached=True))
|
||||
room_members.append(await app.ctx.om.get_order(code=member_id, cached=True))
|
||||
|
||||
tpl = app.ctx.tpl.get_template('welcome.html')
|
||||
return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates))
|
||||
return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates, ROOM_ERROR_MESSAGES=ROOM_ERROR_TYPES))
|
||||
|
||||
|
||||
@app.route("/manage/download_ticket")
|
||||
|
@ -138,19 +173,76 @@ async def download_ticket(request, order: Order):
|
|||
if not order.status != 'confirmed':
|
||||
raise exceptions.Forbidden("You are not allowed to download this ticket.")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
res = await client.get(join(base_url, f"orders/{order.code}/download/pdf/"), headers=headers)
|
||||
res = await pretixClient.get(f"orders/{order.code}/download/pdf/", expectedStatusCodes=[200, 404, 409, 403])
|
||||
|
||||
if res.status_code == 409:
|
||||
if res.status_code == 409 or res.status_code == 404:
|
||||
raise exceptions.SanicException("Your ticket is still being generated. Please try again later!", status_code=res.status_code)
|
||||
elif res.status_code == 403:
|
||||
raise exceptions.SanicException("You can download your ticket only after the order has been confirmed and paid. Try later!", status_code=400)
|
||||
|
||||
return raw(res.content, content_type='application/pdf')
|
||||
|
||||
@app.route("/manage/admin")
|
||||
async def admin(request, order: Order):
|
||||
await request.app.ctx.om.update_cache()
|
||||
if not order:
|
||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
if EXTRA_PRINTS:
|
||||
logger.info(f"Checking admin credentials of {order.code} with secret {order.secret}")
|
||||
if not order.isAdmin(): raise exceptions.Forbidden("Birichino :)")
|
||||
tpl = app.ctx.tpl.get_template('admin.html')
|
||||
return html(tpl.render(order=order))
|
||||
|
||||
@app.route("/manage/logout")
|
||||
async def logour(request):
|
||||
raise exceptions.Forbidden("You have been logged out.", status_code=403)
|
||||
async def logout(request):
|
||||
orgCode = request.cookies.get("foxo_code_ORG")
|
||||
orgSecret = request.cookies.get("foxo_secret_ORG")
|
||||
if orgCode != None and orgSecret != None:
|
||||
r = redirect(f'/manage/welcome')
|
||||
r.cookies['foxo_code'] = orgCode
|
||||
r.cookies['foxo_secret'] = orgSecret
|
||||
r.delete_cookie("foxo_code_ORG")
|
||||
r.delete_cookie("foxo_secret_ORG")
|
||||
return r
|
||||
|
||||
raise exceptions.Forbidden("You have been logged out.")
|
||||
|
||||
@app.signal("server.shutdown.before")
|
||||
async def sigintHandler(app, loop):
|
||||
killSmptClient()
|
||||
|
||||
@app.get(METRICS_PATH)
|
||||
async def metrics(request):
|
||||
return text(getMetricsText() + "\n" + getRoomCountersText(request))
|
||||
|
||||
@app.on_request
|
||||
async def countReqs(request : Request):
|
||||
global METRICS_REQ_NO
|
||||
if(request.path != METRICS_PATH):
|
||||
incReqNo()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8188, dev=DEV_MODE)
|
||||
# Wait for pretix in server reboot
|
||||
# Using a docker configuration, pretix may be unable to talk with postgres if postgres' service started before it.
|
||||
# To fix this issue I added a After=pretix.service to the [Unit] section of /lib/systemd/system/postgresql@.service
|
||||
# to let it start in the correct order. The following piece of code makes sure that pretix is running and can talk to
|
||||
# postgres before actually starting the reserved area, since this operation requires a cache-fill in startup
|
||||
print("Waiting for pretix to be up and running", file=sys.stderr)
|
||||
while not SKIP_HEALTHCHECK:
|
||||
print("Trying connecting to pretix...", file=sys.stderr)
|
||||
try:
|
||||
incPretixRead()
|
||||
res = requests.get(base_url_event, headers=headers)
|
||||
res = res.json()
|
||||
if(res['slug'] == EVENT_NAME):
|
||||
print("Healtchecking...", file=sys.stderr)
|
||||
incPretixRead()
|
||||
res = requests.get(join(domain, "healthcheck"), headers=headers)
|
||||
if(res.status_code == 200):
|
||||
break
|
||||
except:
|
||||
pass
|
||||
sleep(5)
|
||||
print("Connected to pretix!", file=sys.stderr)
|
||||
|
||||
app.run(host="127.0.0.1", port=8188, dev=DEV_MODE, access_log=ACCESS_LOG)
|
||||
|
|
|
@ -10,7 +10,6 @@ async def carpooling_list(request, order: Order, error=None):
|
|||
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
||||
orders = [value for value in request.app.ctx.om.cache.values() if value.status not in ['c', 'e'] and value.carpooling_message]
|
||||
print(orders)
|
||||
|
||||
tpl = request.app.ctx.tpl.get_template('carpooling.html')
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from sanic.response import html, redirect, text
|
|||
from sanic import Blueprint, exceptions, response
|
||||
from random import choice
|
||||
from ext import *
|
||||
from config import headers, base_url
|
||||
from config import headers, base_url_event
|
||||
from PIL import Image
|
||||
from os.path import isfile
|
||||
from os import unlink
|
||||
|
@ -11,6 +11,8 @@ from hashlib import sha224
|
|||
from time import time
|
||||
from urllib.parse import unquote
|
||||
import json
|
||||
from metrics import *
|
||||
import pretixClient
|
||||
|
||||
bp = Blueprint("checkin", url_prefix="/checkin")
|
||||
|
||||
|
@ -63,8 +65,7 @@ async def do_checkin(request):
|
|||
await order.send_answers()
|
||||
|
||||
if not order.checked_in:
|
||||
async with httpx.AsyncClient() as client:
|
||||
res = await client.post(base_url.replace('events/beyond/', 'checkinrpc/redeem/'), json={'secret': order.barcode, 'source_type': 'barcode', 'type': 'entry', 'lists': [3,]}, headers=headers)
|
||||
await pretixClient.post("", baseUrl=base_url_event.replace(f'events/{EVENT_NAME}/', 'checkinrpc/redeem/'), json={'secret': order.barcode, 'source_type': 'barcode', 'type': 'entry', 'lists': [3,]})
|
||||
|
||||
tpl = request.app.ctx.tpl.get_template('checkin_3.html')
|
||||
return html(tpl.render(order=order, room_owner=room_owner, roommates=roommates))
|
||||
|
|
|
@ -1,41 +1,154 @@
|
|||
from sanic.log import logging
|
||||
LOG_LEVEL = logging.DEBUG
|
||||
|
||||
API_TOKEN = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
||||
ORGANIZER = 'furizon'
|
||||
EVENT_NAME = 'river-side-2023'
|
||||
API_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxx'
|
||||
HOSTNAME = 'your-pretix-hostname'
|
||||
EVENT_NAME = 'overlord'
|
||||
HOSTNAME = 'reg.furizon.net'
|
||||
SKIP_HEALTHCHECK = False
|
||||
|
||||
headers = {'Host': HOSTNAME, 'Authorization': f'Token {API_TOKEN}'}
|
||||
base_url = f"https://{HOSTNAME}/api/v1/organizers/{ORGANIZER}/events/{EVENT_NAME}/"
|
||||
domain = "http://urlllllllllllllllllllll/"
|
||||
base_url = "{domain}api/v1/"
|
||||
base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/"
|
||||
|
||||
PROPIC_DEADLINE = 1683575684
|
||||
FILL_CACHE = True
|
||||
|
||||
DEV_MODE = True
|
||||
|
||||
ITEM_IDS = {
|
||||
'ticket': [90,],
|
||||
'membership_card': [91,],
|
||||
'sponsorship': [], # first one = normal, second = super
|
||||
'early_arrival': [],
|
||||
'late_departure': [],
|
||||
'room': 98
|
||||
}
|
||||
|
||||
# Create a bunch of "room" items which will get added to the order once somebody gets a room.
|
||||
ROOM_MAP = {
|
||||
1: 16,
|
||||
2: 17,
|
||||
3: 18,
|
||||
4: 19,
|
||||
5: 20
|
||||
}
|
||||
PROPIC_DEADLINE = 9999999999
|
||||
PROPIC_MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
|
||||
PROPIC_MAX_SIZE = (2048, 2048) # (Width, Height)
|
||||
PROPIC_MIN_SIZE = (125, 125) # (Width, Height)
|
||||
|
||||
# This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id.
|
||||
TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
||||
TG_CHAT_ID = -1234567
|
||||
|
||||
import httpx
|
||||
# Number of tries for a request to the pretix's backend
|
||||
PRETIX_REQUESTS_MAX = 3
|
||||
PRETIX_REQUESTS_TIMEOUT = httpx.Timeout(15.0, read=30.0, connect=45.0, pool=None) # Timeout for httpx requests in seconds
|
||||
|
||||
# These order codes have additional functions.
|
||||
ADMINS = ['XXXXX', 'YYYYY']
|
||||
# A list of staff_roles
|
||||
ADMINS_PRETIX_ROLE_NAMES = ["Reserved Area admin", "main staff"]
|
||||
|
||||
SMTP_HOST = 'your-smtp-host.com'
|
||||
SMTP_USER = 'username'
|
||||
SMTP_PASSWORD = 'password'
|
||||
SMTP_HOST = 'host'
|
||||
SMTP_PORT = 0
|
||||
SMTP_USER = 'user'
|
||||
SMTP_PASSWORD = 'pw'
|
||||
EMAIL_SENDER_NAME = "Fantastic Furcon Wow"
|
||||
EMAIL_SENDER_MAIL = "no-reply@thisIsAFantasticFurconWowItIsWonderful.cuteFurries.ovh"
|
||||
SMPT_CLIENT_CLOSE_TIMEOUT = 60 * 15 # 15 minutes
|
||||
|
||||
FILL_CACHE = True
|
||||
CACHE_EXPIRE_TIME = 60 * 60 * 4
|
||||
|
||||
DEV_MODE = True
|
||||
ACCESS_LOG = True
|
||||
EXTRA_PRINTS = True
|
||||
|
||||
UNCONFIRM_ROOMS_ENABLE = True
|
||||
|
||||
METRICS_PATH = "/welcome/metrics"
|
||||
|
||||
# Additional configured locales.
|
||||
# If an order has a country that's not listed here,
|
||||
# Will default to an english preference.
|
||||
AVAILABLE_LOCALES = ['it',]
|
||||
|
||||
# Metadata property for item-id mapping
|
||||
METADATA_NAME = "item_name"
|
||||
# Metadata property for internal category mapping (not related to pretix's category)
|
||||
METADATA_CATEGORY = "category_name"
|
||||
|
||||
SPONSORSHIP_COLOR_MAP = {
|
||||
'super': (251, 140, 0),
|
||||
'normal': (142, 36, 170)
|
||||
}
|
||||
|
||||
# Quotes
|
||||
QUOTES_LIST = []
|
||||
|
||||
# Maps Products metadata name <--> ID
|
||||
ITEMS_ID_MAP = {
|
||||
'early_bird_ticket': None,
|
||||
'regular_ticket': None,
|
||||
'staff_ticket': None,
|
||||
'daily_ticket': None,
|
||||
'regular_bundle_sponsor_ticket': None,
|
||||
'sponsorship_item': None,
|
||||
'early_arrival_admission': None,
|
||||
'late_departure_admission': None,
|
||||
'membership_card_item': None,
|
||||
'bed_in_room': None,
|
||||
'room_type': None,
|
||||
'room_guest': None,
|
||||
'daily_1': None,
|
||||
'daily_2': None,
|
||||
'daily_3': None,
|
||||
'daily_4': None,
|
||||
'daily_5': None
|
||||
}
|
||||
|
||||
# Maps Products' variants metadata name <--> ID
|
||||
ITEM_VARIATIONS_MAP = {
|
||||
'sponsorship_item': {
|
||||
'sponsorship_item_normal': None,
|
||||
'sponsorship_item_super': None
|
||||
},
|
||||
'bed_in_room': {
|
||||
'bed_in_room_no_room': None,
|
||||
'bed_in_room_main_1': None,
|
||||
'bed_in_room_main_2': None,
|
||||
'bed_in_room_main_3': None,
|
||||
'bed_in_room_main_4': None,
|
||||
'bed_in_room_main_5': None,
|
||||
'bed_in_room_overflow1_2': None,
|
||||
},
|
||||
'room_type': {
|
||||
'single': None,
|
||||
'double': None,
|
||||
'triple': None,
|
||||
'quadruple': None,
|
||||
'quintuple': None
|
||||
},
|
||||
'room_guest': {
|
||||
'single': None,
|
||||
'double': None,
|
||||
'triple': None,
|
||||
'quadruple': None,
|
||||
'quintuple': None
|
||||
}
|
||||
}
|
||||
|
||||
ADMINS_PRETIX_ROLE_NAMES = ["Reserved Area admin", "main staff"]
|
||||
|
||||
# Links Products' variants' ids with the internal category name
|
||||
CATEGORIES_LIST_MAP = {
|
||||
'tickets': [],
|
||||
'memberships': [],
|
||||
'sponsorships': [],
|
||||
'tshirts': [],
|
||||
'extra_days': [],
|
||||
'rooms': [],
|
||||
'dailys': []
|
||||
}
|
||||
|
||||
# Create a bunch of "room" items which will get added to the order once somebody gets a room.
|
||||
# Map item_name -> room capacity
|
||||
ROOM_CAPACITY_MAP = {
|
||||
# Default
|
||||
'bed_in_room_no_room': 0,
|
||||
|
||||
# SACRO CUORE
|
||||
'bed_in_room_main_1': 1,
|
||||
'bed_in_room_main_2': 2,
|
||||
'bed_in_room_main_3': 3,
|
||||
'bed_in_room_main_4': 4,
|
||||
'bed_in_room_main_5': 5,
|
||||
|
||||
# OVERFLOW 1
|
||||
'bed_in_room_overflow1_2': 2,
|
||||
}
|
||||
|
||||
# Autofilled. Maps roomTypeId -> roomName
|
||||
ROOM_TYPE_NAMES = { }
|
|
@ -0,0 +1,21 @@
|
|||
# To connect with pretix's data
|
||||
# WIP to not commit
|
||||
# DrewTW
|
||||
import string
|
||||
import httpx
|
||||
import json
|
||||
from ext import *
|
||||
from config import *
|
||||
from sanic import response
|
||||
from sanic import Blueprint
|
||||
from sanic.log import logger
|
||||
|
||||
def checkConfig():
|
||||
if (not DEV_MODE) and DUMMY_DATA:
|
||||
logger.warn('It is strongly unadvised to use dummy data in production')
|
||||
|
||||
def getOrders(page):
|
||||
return None
|
||||
|
||||
def getOrder(code):
|
||||
return None
|
BIN
data/boop.db
BIN
data/event.db
|
@ -0,0 +1,124 @@
|
|||
from sanic import Sanic
|
||||
from sanic.log import logger
|
||||
import ssl
|
||||
from ssl import SSLContext
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from messages import ROOM_ERROR_TYPES
|
||||
import smtplib
|
||||
from messages import *
|
||||
from config import *
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from threading import Timer, Lock
|
||||
|
||||
def killSmptClient():
|
||||
global sslLock
|
||||
global sslTimer
|
||||
global smptSender
|
||||
sslTimer.cancel()
|
||||
sslLock.acquire()
|
||||
if(smptSender is not None):
|
||||
logger.debug('[SMPT] Closing smpt client')
|
||||
smptSender.quit() # it calls close() inside
|
||||
smptSender = None
|
||||
sslLock.release()
|
||||
|
||||
async def openSmptClient():
|
||||
global sslLock
|
||||
global sslTimer
|
||||
global sslContext
|
||||
global smptSender
|
||||
sslTimer.cancel()
|
||||
sslLock.acquire()
|
||||
if(smptSender is None):
|
||||
logger.debug('[SMPT] Opening smpt client')
|
||||
client : smtplib.SMTP = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
|
||||
client.starttls(context=sslContext)
|
||||
client.login(SMTP_USER, SMTP_PASSWORD)
|
||||
smptSender = client
|
||||
sslLock.release()
|
||||
sslTimer = createTimer()
|
||||
sslTimer.start()
|
||||
|
||||
def createTimer():
|
||||
return Timer(SMPT_CLIENT_CLOSE_TIMEOUT, killSmptClient)
|
||||
sslLock : Lock = Lock()
|
||||
sslTimer : Timer = createTimer()
|
||||
sslContext : SSLContext = ssl.create_default_context()
|
||||
smptSender : smtplib.SMTP = None
|
||||
|
||||
async def sendEmail(message : MIMEMultipart):
|
||||
message['From'] = f'{EMAIL_SENDER_NAME} <{EMAIL_SENDER_MAIL}>'
|
||||
await openSmptClient()
|
||||
logger.debug(f"[SMPT] Sending mail {message['From']} -> {message['to']} '{message['Subject']}'")
|
||||
sslLock.acquire()
|
||||
smptSender.sendmail(message['From'], message['to'], message.as_string())
|
||||
sslLock.release()
|
||||
|
||||
def render_email_template(title = "", body = ""):
|
||||
tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html')
|
||||
return str(tpl.render(title=title, body=body))
|
||||
|
||||
|
||||
|
||||
|
||||
async def send_unconfirm_message(room_order, orders):
|
||||
memberMessages = []
|
||||
|
||||
issues_plain = ""
|
||||
issues_html = "<ul>"
|
||||
|
||||
for err in room_order.room_errors:
|
||||
errId = err[1]
|
||||
order = err[0]
|
||||
orderStr = ""
|
||||
if order is not None:
|
||||
orderStr = f"{order}: "
|
||||
if errId in ROOM_ERROR_TYPES.keys():
|
||||
issues_plain += f" • {orderStr}{ROOM_ERROR_TYPES[errId]}\n"
|
||||
issues_html += f"<li>{orderStr}{ROOM_ERROR_TYPES[errId]}</li>"
|
||||
issues_html += "</ul>"
|
||||
|
||||
for member in orders:
|
||||
if(member.status != 'canceled'):
|
||||
plain_body = EMAILS_TEXT["ROOM_UNCONFIRM_TEXT"]['plain'].format(member.name, room_order.room_name, issues_plain)
|
||||
html_body = render_email_template(EMAILS_TEXT["ROOM_UNCONFIRM_TITLE"], EMAILS_TEXT["ROOM_UNCONFIRM_TEXT"]['html'].format(member.name, room_order.room_name, issues_html))
|
||||
plain_text = MIMEText(plain_body, "plain")
|
||||
html_text = MIMEText(html_body, "html")
|
||||
message = MIMEMultipart("alternative")
|
||||
message.attach(plain_text)
|
||||
message.attach(html_text)
|
||||
message['Subject'] = f'[{EMAIL_SENDER_NAME}] Your room cannot be confirmed'
|
||||
message['To'] = f"{member.name} <{member.email}>"
|
||||
memberMessages.append(message)
|
||||
|
||||
if len(memberMessages) == 0: return
|
||||
|
||||
for message in memberMessages:
|
||||
await sendEmail(message)
|
||||
|
||||
async def send_missing_propic_message(order, missingPropic, missingFursuitPropic):
|
||||
t = []
|
||||
if(missingPropic): t.append("your propic")
|
||||
if(missingFursuitPropic): t.append("your fursuit's badge")
|
||||
missingText = " and ".join(t)
|
||||
|
||||
plain_body = EMAILS_TEXT["MISSING_PROPIC_TEXT"]['plain'].format(order.name, missingText)
|
||||
html_body = render_email_template(EMAILS_TEXT["MISSING_PROPIC_TITLE"], EMAILS_TEXT["MISSING_PROPIC_TEXT"]['html'].format(order.name, missingText))
|
||||
plain_text = MIMEText(plain_body, "plain")
|
||||
html_text = MIMEText(html_body, "html")
|
||||
message = MIMEMultipart("alternative")
|
||||
message.attach(plain_text)
|
||||
message.attach(html_text)
|
||||
message['Subject'] = f"[{EMAIL_SENDER_NAME}] You haven't uploaded your badges yet!"
|
||||
message['To'] = f"{order.name} <{order.email}>"
|
||||
|
||||
await sendEmail(message)
|
||||
|
||||
async def send_app_login_attempt(user, loginCode):
|
||||
#TODO: Format a proper email and add it to messages.py
|
||||
msg = MIMEText(f"Hello {user.name}!\n\nWe have received a request to login in the app. If you didn't do this, please ignore this email. Somebody is probably playing with you.\n\nYour login code is: {loginCode}\n\nPlease do not tell this to anybody!")
|
||||
msg['Subject'] = '[Furizon] Your login code'
|
||||
msg['To'] = f"{user.name} <{user.email}>"
|
||||
|
||||
await sendEmail(msg)
|
87
export.py
|
@ -1,87 +0,0 @@
|
|||
from sanic.response import text
|
||||
from sanic import Blueprint, exceptions
|
||||
from ext import *
|
||||
from config import headers, ADMINS, ORGANIZER, EVENT_NAME
|
||||
|
||||
bp = Blueprint("export", url_prefix="/manage/export")
|
||||
|
||||
@bp.route("/export.csv")
|
||||
async def export_csv(request, order: Order):
|
||||
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
if order.code not in ADMINS: raise exceptions.Forbidden("Birichino :)")
|
||||
|
||||
page = 0
|
||||
orders = {}
|
||||
|
||||
ret = 'status;code;nome;cognome;nick;nazione;tessera;artista;fursuiter;sponsorship;early;late;shirt;roomsize;roommembers;payment;price;refunds;staff\n'
|
||||
|
||||
while 1:
|
||||
page += 1
|
||||
|
||||
r = httpx.get(f'https://reg.furizon.net/api/v1/organizers/{ORGANIZER}/events/{EVENT_NAME}/orders/?page={page}', headers=headers)
|
||||
if r.status_code == 404: break
|
||||
|
||||
for r in r.json()['results']:
|
||||
|
||||
o = Order(r)
|
||||
orders[o.code] = o
|
||||
|
||||
ret += (';'.join(map(lambda x: str(x),
|
||||
[
|
||||
o.status,
|
||||
o.code,
|
||||
o.first_name,
|
||||
o.last_name,
|
||||
o.name,
|
||||
o.country,
|
||||
o.has_card or '',
|
||||
o.is_artist or '',
|
||||
o.is_fursuiter or '',
|
||||
o.sponsorship or '',
|
||||
o.has_early or '',
|
||||
o.has_late or '',
|
||||
o.shirt_size,
|
||||
len(o.room_members),
|
||||
','.join(o.room_members),
|
||||
o.payment_provider,
|
||||
o.total-o.fees,
|
||||
o.refunds,
|
||||
o.ans('staff_role') or 'attendee',
|
||||
]))) + "\n"
|
||||
|
||||
return text(ret)
|
||||
|
||||
@bp.route("/hotel_export.csv")
|
||||
async def export_hotel_csv(request, order: Order):
|
||||
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
if order.code not in ['HWUC9','9YKGJ']: raise exceptions.Forbidden("Birichino :)")
|
||||
|
||||
page = 0
|
||||
orders = {}
|
||||
|
||||
ret = 'code;nome;cognome;datanascita;posnascita;indirizzo;mail;status\n'
|
||||
|
||||
while 1:
|
||||
page += 1
|
||||
|
||||
r = httpx.get(f'https://reg.furizon.net/api/v1/organizers/{ORGANIZER}/events/{EVENT_NAME}/orders/?page={page}', headers=headers)
|
||||
if r.status_code == 404: break
|
||||
|
||||
for r in r.json()['results']:
|
||||
|
||||
o = Order(r)
|
||||
orders[o.code] = o
|
||||
|
||||
ret += (';'.join(map(lambda x: str(x),
|
||||
[
|
||||
o.code,
|
||||
o.first_name,
|
||||
o.last_name,
|
||||
o.birth_date,
|
||||
o.birth_location,
|
||||
o.address,
|
||||
o.email,
|
||||
o.status
|
||||
]))) + "\n"
|
||||
|
||||
return text(ret)
|
354
ext.py
|
@ -1,11 +1,18 @@
|
|||
from dataclasses import dataclass
|
||||
from sanic import Request, exceptions
|
||||
from sanic import Request, exceptions, Sanic
|
||||
import httpx
|
||||
import re
|
||||
from utils import *
|
||||
from config import *
|
||||
from os.path import join
|
||||
import json
|
||||
from sanic.log import logger
|
||||
from time import time
|
||||
from metrics import *
|
||||
import asyncio
|
||||
from threading import Lock
|
||||
import pretixClient
|
||||
import traceback
|
||||
|
||||
@dataclass
|
||||
class Order:
|
||||
|
@ -13,8 +20,13 @@ class Order:
|
|||
|
||||
self.time = time()
|
||||
self.data = data
|
||||
if(len(self.data['positions']) == 0):
|
||||
for fee in data['fees']:
|
||||
if(fee['fee_type'] == "cancellation"):
|
||||
self.data['status'] = 'c'
|
||||
self.status = {'n': 'pending', 'p': 'paid', 'e': 'expired', 'c': 'canceled'}[self.data['status']]
|
||||
|
||||
self.secret = data['secret']
|
||||
|
||||
if not len(self.data['positions']):
|
||||
self.status = 'canceled'
|
||||
|
||||
|
@ -27,41 +39,65 @@ class Order:
|
|||
self.sponsorship = None
|
||||
self.has_early = False
|
||||
self.has_late = False
|
||||
self.first_name = None
|
||||
self.last_name = None
|
||||
self.first_name = "None"
|
||||
self.last_name = "None"
|
||||
self.country = 'xx'
|
||||
self.address = None
|
||||
self.checked_in = False
|
||||
self.room_type = None
|
||||
self.daily = False
|
||||
self.dailyDays = []
|
||||
self.bed_in_room = -1
|
||||
self.room_person_no = -1
|
||||
self.answers = []
|
||||
self.position_id = -1
|
||||
self.position_positionid = -1
|
||||
self.position_positiontypeid = -1
|
||||
self.barcode = "None"
|
||||
|
||||
idata = data['invoice_address']
|
||||
if idata:
|
||||
self.address = f"{idata['street']} - {idata['zipcode']} {idata['city']} - {idata['country']}"
|
||||
self.address = f"{idata['street'].strip()} - {idata['zipcode'].strip()} {idata['city'].strip()} - {idata['country'].strip()}".replace("\n", "").replace("\r", "")
|
||||
self.country = idata['country']
|
||||
|
||||
for p in self.data['positions']:
|
||||
if p['item'] in (ITEM_IDS['ticket'] + ITEM_IDS['daily']):
|
||||
if p['item'] in CATEGORIES_LIST_MAP['tickets']:
|
||||
self.position_id = p['id']
|
||||
self.position_positionid = p['positionid']
|
||||
self.position_positiontypeid = p['item']
|
||||
self.answers = p['answers']
|
||||
for i, ans in enumerate(self.answers):
|
||||
if(TYPE_OF_QUESTIONS[self.answers[i]['question']] == QUESTION_TYPES['file_upload']):
|
||||
self.answers[i]['answer'] = "file:keep"
|
||||
self.barcode = p['secret']
|
||||
self.checked_in = bool(p['checkins'])
|
||||
|
||||
if p['item'] in CATEGORIES_LIST_MAP['dailys']:
|
||||
self.daily = True
|
||||
self.dailyDays.append(CATEGORIES_LIST_MAP['dailys'].index(p['item']))
|
||||
|
||||
if p['item'] in ITEM_IDS['membership_card']:
|
||||
if p['item'] in CATEGORIES_LIST_MAP['memberships']:
|
||||
self.has_card = True
|
||||
|
||||
if p['item'] in ITEM_IDS['sponsorship']:
|
||||
self.sponsorship = 'normal' if p['variation'] == ITEMS_IDS['sponsorship'][0] else 'super'
|
||||
if p['item'] == ITEMS_ID_MAP['sponsorship_item']:
|
||||
sponsorshipType = key_from_value(ITEM_VARIATIONS_MAP['sponsorship_item'], p['variation'])
|
||||
self.sponsorship = sponsorshipType[0].replace ('sponsorship_item_', '') if len(sponsorshipType) > 0 else None
|
||||
|
||||
if p['attendee_name']:
|
||||
self.first_name = p['attendee_name_parts']['given_name']
|
||||
self.last_name = p['attendee_name_parts']['family_name']
|
||||
|
||||
if p['item'] == ITEM_IDS['early_arrival']:
|
||||
if p['item'] == ITEMS_ID_MAP['early_arrival_admission']:
|
||||
self.has_early = True
|
||||
|
||||
if p['item'] == ITEM_IDS['late_departure']:
|
||||
if p['item'] == ITEMS_ID_MAP['late_departure_admission']:
|
||||
self.has_late = True
|
||||
|
||||
if p['item'] == ITEMS_ID_MAP['bed_in_room']:
|
||||
roomTypeLst = key_from_value(ITEM_VARIATIONS_MAP['bed_in_room'], p['variation'])
|
||||
roomTypeId = roomTypeLst[0] if len(roomTypeLst) > 0 else None
|
||||
self.bed_in_room = p['variation']
|
||||
self.room_person_no = ROOM_CAPACITY_MAP[roomTypeId] if roomTypeId in ROOM_CAPACITY_MAP else self.room_person_no
|
||||
|
||||
self.total = float(data['total'])
|
||||
self.fees = 0
|
||||
|
@ -77,6 +113,12 @@ class Order:
|
|||
self.payment_provider = data['payment_provider']
|
||||
self.comment = data['comment']
|
||||
self.phone = data['phone']
|
||||
self.room_errors = []
|
||||
self.loadAns()
|
||||
|
||||
if(self.bed_in_room < 0 and not self.daily):
|
||||
self.status = "canceled" # Must refer to the previous status assignment
|
||||
def loadAns(self):
|
||||
self.shirt_size = self.ans('shirt_size')
|
||||
self.is_artist = True if self.ans('is_artist') != 'No' else False
|
||||
self.is_fursuiter = True if self.ans('is_fursuiter') != 'No' else False
|
||||
|
@ -97,16 +139,20 @@ class Order:
|
|||
self.pending_room = self.ans('pending_room')
|
||||
self.pending_roommates = self.ans('pending_roommates').split(',') if self.ans('pending_roommates') else []
|
||||
self.room_members = self.ans('room_members').split(',') if self.ans('room_members') else []
|
||||
self.room_owner = (self.code == self.room_id)
|
||||
self.room_owner = (self.code is not None and self.room_id is not None and self.code.strip() == self.room_id.strip())
|
||||
self.room_secret = self.ans('room_secret')
|
||||
self.app_token = self.ans('app_token')
|
||||
self.nfc_id = self.ans('nfc_id')
|
||||
self.can_scan_nfc = True if self.ans('can_scan_nfc') != 'No' else False
|
||||
self.actual_room = self.ans('actual_room')
|
||||
self.staff_role = self.ans('staff_role')
|
||||
self.telegram_username = self.ans('telegram_username').strip('@') if self.ans('telegram_username') else None
|
||||
|
||||
self.shuttle_bus = self.ans('shuttle_bus')
|
||||
def __getitem__(self, var):
|
||||
return self.data[var]
|
||||
|
||||
def set_room_errors (self, to_set):
|
||||
self.room_errors = to_set
|
||||
|
||||
def ans(self, name):
|
||||
for p in self.data['positions']:
|
||||
|
@ -116,59 +162,129 @@ class Order:
|
|||
return bool(a['answer'] == 'True')
|
||||
return a['answer']
|
||||
return None
|
||||
|
||||
def isBadgeValid (self):
|
||||
return self.ans('propic') and (not self.is_fursuiter or self.ans('propic_fursuiter'))
|
||||
|
||||
def isAdmin (self):
|
||||
return self.code in ADMINS or self.staff_role in ADMINS_PRETIX_ROLE_NAMES
|
||||
|
||||
async def edit_answer_fileUpload(self, name, fileName, mimeType, data : bytes):
|
||||
if(mimeType != None and data != None):
|
||||
localHeaders = dict(headers)
|
||||
localHeaders['Content-Type'] = mimeType
|
||||
localHeaders['Content-Disposition'] = f'attachment; filename="{fileName}"'
|
||||
res = await pretixClient.post("upload", baseUrl=base_url, headers=localHeaders, content=data, expectedStatusCodes=[201])
|
||||
res = res.json()
|
||||
await self.edit_answer(name, res['id'])
|
||||
else:
|
||||
await self.edit_answer(name, None)
|
||||
self.loadAns()
|
||||
|
||||
async def edit_answer(self, name, new_answer):
|
||||
found = False
|
||||
self.pending_update = True
|
||||
for key in range(len(self.answers)):
|
||||
if self.answers[key].get('question_identifier', None) == name:
|
||||
if new_answer != None:
|
||||
print('EXISTING ANSWER UPDATE', name, '=>', new_answer)
|
||||
if DEV_MODE and EXTRA_PRINTS: logger.debug('[ANSWER EDIT] EXISTING ANSWER UPDATE %s => %s', name, new_answer)
|
||||
self.answers[key]['answer'] = new_answer
|
||||
found = True
|
||||
else:
|
||||
print('DEL ANSWER', name, '=>', new_answer)
|
||||
if DEV_MODE and EXTRA_PRINTS: logger.debug('[ANSWER EDIT] DEL ANSWER %s => %s', name, new_answer)
|
||||
del self.answers[key]
|
||||
|
||||
break
|
||||
|
||||
if (not found) and (new_answer is not None):
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
res = await client.get(join(base_url, 'questions/'), headers=headers)
|
||||
res = res.json()
|
||||
|
||||
res = await pretixClient.get("questions/")
|
||||
res = res.json()
|
||||
for r in res['results']:
|
||||
if r['identifier'] != name: continue
|
||||
|
||||
print('ANSWER UPDATE', name, '=>', new_answer)
|
||||
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'[ANSWER EDIT] %s => %s', name, new_answer)
|
||||
self.answers.append({
|
||||
'question': r['id'],
|
||||
'answer': new_answer,
|
||||
'options': r['options']
|
||||
})
|
||||
self.loadAns()
|
||||
|
||||
async def send_answers(self):
|
||||
async with httpx.AsyncClient() as client:
|
||||
print("POSITION ID IS", self.position_id)
|
||||
|
||||
for i, ans in enumerate(self.answers):
|
||||
# Fix for karaoke fields
|
||||
if ans['question'] == 40:
|
||||
del self.answers[i]['options']
|
||||
del self.answers[i]['option_identifiers']
|
||||
|
||||
res = await client.patch(join(base_url, f'orderpositions/{self.position_id}/'), headers=headers, json={'answers': self.answers})
|
||||
|
||||
if res.status_code != 200:
|
||||
if DEV_MODE and EXTRA_PRINTS: logger.debug("[ANSWER POST] POSITION ID IS %s", self.position_id)
|
||||
|
||||
for i, ans in enumerate(self.answers):
|
||||
if TYPE_OF_QUESTIONS[ans['question']] == QUESTION_TYPES["multiple_choice_from_list"]: # if multiple choice
|
||||
identifier = ans['question_identifier']
|
||||
if self.ans(identifier) == "": #if empty answer
|
||||
await self.edit_answer(identifier, None)
|
||||
# Fix for karaoke fields
|
||||
#if ans['question'] == 40:
|
||||
# del self.answers[i]['options']
|
||||
# del self.answers[i]['option_identifiers']
|
||||
|
||||
ans = [] if self.status == "canceled" else self.answers
|
||||
res = await pretixClient.patch(f'orderpositions/{self.position_id}/', json={'answers': ans}, expectedStatusCodes=None)
|
||||
|
||||
if res.status_code != 200:
|
||||
e = res.json()
|
||||
if "answers" in e:
|
||||
for ans, err in zip(self.answers, res.json()['answers']):
|
||||
if err:
|
||||
print('ERROR ON', ans, err)
|
||||
logger.error ('[ANSWERS SENDING] ERROR ON %s %s', ans, err)
|
||||
else:
|
||||
logger.error("[ANSWERS SENDING] GENERIC ERROR. Response: '%s'", str(e))
|
||||
|
||||
raise exceptions.ServerError('There has been an error while updating this answers.')
|
||||
raise exceptions.ServerError('There has been an error while updating this answers.')
|
||||
|
||||
for i, ans in enumerate(self.answers):
|
||||
if(TYPE_OF_QUESTIONS[self.answers[i]['question']] == QUESTION_TYPES['file_upload']):
|
||||
self.answers[i]['answer'] = "file:keep"
|
||||
|
||||
self.pending_update = False
|
||||
self.time = -1
|
||||
self.loadAns()
|
||||
|
||||
def get_language(self):
|
||||
return self.country.lower() if self.country.lower() in AVAILABLE_LOCALES else 'en'
|
||||
|
||||
def __str__(self):
|
||||
to_return = f"{'Room' if self.room_owner else 'Order'} {self.code}"
|
||||
if self.room_owner == True:
|
||||
to_return = f"{to_return} [ members = {self.room_members} ]"
|
||||
return to_return
|
||||
|
||||
def __repr__(self):
|
||||
to_return = f"{'Room' if self.room_owner == True else 'Order'} {self.code}"
|
||||
if self.room_owner == True:
|
||||
to_return = f"{to_return} [ members = {self.room_members} ]"
|
||||
return to_return
|
||||
|
||||
@dataclass
|
||||
class Quota:
|
||||
def __init__(self, data):
|
||||
self.items = data['items'] if 'items' in data else []
|
||||
self.variations = data['variations'] if 'variations' in data else []
|
||||
self.available = data['available'] if 'available' in data else False
|
||||
self.size = data['size'] if 'size' in data else 0
|
||||
self.available_number = data['available_number'] if 'available_number' in data else 0
|
||||
|
||||
def has_item (self, id: int=-1, variation: int=None):
|
||||
return id in self.items if not variation else (id in self.items and variation in self.variations)
|
||||
|
||||
def get_left (self):
|
||||
return self.available_number
|
||||
|
||||
def __repr__(self):
|
||||
return f'Quota [items={self.items}, variations={self.variations}] [{self.available_number}/{self.size}]'
|
||||
|
||||
def __str__(self):
|
||||
return f'Quota [items={self.items}, variations={self.variations}] [{self.available_number}/{self.size}]'
|
||||
|
||||
def get_quota(item: int, variation: int = None) -> Quota:
|
||||
for q in QUOTA_LIST:
|
||||
if (q.has_item(item, variation)): return q
|
||||
return None
|
||||
|
||||
@dataclass
|
||||
class Quotas:
|
||||
|
@ -182,11 +298,25 @@ class Quotas:
|
|||
return 0
|
||||
|
||||
async def get_quotas(request: Request=None):
|
||||
async with httpx.AsyncClient() as client:
|
||||
res = await client.get(join(base_url, 'quotas/?order=id&with_availability=true'), headers=headers)
|
||||
res = await pretixClient.get('quotas/?order=id&with_availability=true')
|
||||
res = res.json()
|
||||
|
||||
return Quotas(res)
|
||||
|
||||
async def load_item_quotas() -> bool:
|
||||
global QUOTA_LIST
|
||||
QUOTA_LIST = []
|
||||
logger.info ('[QUOTAS] Loading quotas...')
|
||||
success = True
|
||||
try:
|
||||
res = await pretixClient.get('quotas/?order=id&with_availability=true')
|
||||
res = res.json()
|
||||
|
||||
return Quotas(res)
|
||||
for quota_data in res['results']:
|
||||
QUOTA_LIST.append (Quota(quota_data))
|
||||
except Exception:
|
||||
logger.warning(f"[QUOTAS] Error while loading quotas.\n{traceback.format_exc()}")
|
||||
success = False
|
||||
return success
|
||||
|
||||
async def get_order(request: Request=None):
|
||||
await request.receive_body()
|
||||
|
@ -194,37 +324,105 @@ async def get_order(request: Request=None):
|
|||
|
||||
class OrderManager:
|
||||
def __init__(self):
|
||||
self.lastCacheUpdate = 0
|
||||
self.updating : Lock = Lock()
|
||||
self.empty()
|
||||
|
||||
def empty(self):
|
||||
self.cache = {}
|
||||
self.order_list = []
|
||||
|
||||
def add_cache(self, order):
|
||||
self.cache[order.code] = order
|
||||
if not order.code in self.order_list:
|
||||
self.order_list.append(order.code)
|
||||
# Will fill cache once the last cache update is greater than cache expire time
|
||||
async def update_cache(self, check_itemsQuestions=False):
|
||||
t = time()
|
||||
to_return = False
|
||||
success = True
|
||||
if(t - self.lastCacheUpdate > CACHE_EXPIRE_TIME and not self.updating.locked()):
|
||||
to_return = True
|
||||
success = await self.fill_cache(check_itemsQuestions=check_itemsQuestions)
|
||||
return (to_return, success)
|
||||
|
||||
def remove_cache(self, code):
|
||||
if code in self.cache:
|
||||
del self.cache[code]
|
||||
self.order_list.remove(code)
|
||||
def add_cache(self, order, cache=None, orderList=None):
|
||||
# Extra params for dry runs
|
||||
if(cache is None):
|
||||
cache = self.cache
|
||||
if(orderList is None):
|
||||
orderList = self.order_list
|
||||
|
||||
cache[order.code] = order
|
||||
if not order.code in orderList:
|
||||
orderList.append(order.code)
|
||||
|
||||
def remove_cache(self, code, cache=None, orderList=None):
|
||||
# Extra params for dry runs
|
||||
if(cache is None):
|
||||
cache = self.cache
|
||||
if(orderList is None):
|
||||
orderList = self.order_list
|
||||
|
||||
if code in cache:
|
||||
del cache[code]
|
||||
orderList.remove(code)
|
||||
|
||||
async def fill_cache(self):
|
||||
async def fill_cache(self, check_itemsQuestions=False) -> bool:
|
||||
# Check cache lock
|
||||
self.updating.acquire()
|
||||
start_time = time()
|
||||
logger.info("[CACHE] Filling cache...")
|
||||
# Index item's ids
|
||||
r = await load_items()
|
||||
if(not r and check_itemsQuestions):
|
||||
logger.error("[CACHE] Items were not loading correctly. Aborting filling cache...")
|
||||
return False
|
||||
|
||||
# Index questions' types
|
||||
r = await load_questions()
|
||||
if(not r and check_itemsQuestions):
|
||||
logger.error("[CACHE] Questions were not loading correctly. Aborting filling cache...")
|
||||
return False
|
||||
|
||||
# Load quotas
|
||||
r = await load_item_quotas()
|
||||
if(not r and check_itemsQuestions):
|
||||
logger.error("[CACHE] Quotas were not loading correctly. Aborting filling cache...")
|
||||
return False
|
||||
|
||||
cache = {}
|
||||
orderList = []
|
||||
success = True
|
||||
p = 0
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
while 1:
|
||||
p += 1
|
||||
res = await client.get(join(base_url, f"orders/?page={p}"), headers=headers)
|
||||
|
||||
res = await pretixClient.get(f"orders/?page={p}", expectedStatusCodes=[200, 404])
|
||||
if res.status_code == 404: break
|
||||
|
||||
# Parse order data
|
||||
data = res.json()
|
||||
for o in data['results']:
|
||||
o = Order(o)
|
||||
if o.status in ['canceled', 'expired']:
|
||||
self.remove_cache(o.code)
|
||||
self.remove_cache(o.code, cache=cache, orderList=orderList)
|
||||
else:
|
||||
self.add_cache(Order(o))
|
||||
|
||||
self.add_cache(Order(o), cache=cache, orderList=orderList)
|
||||
self.lastCacheUpdate = time()
|
||||
logger.info(f"[CACHE] Cache filled in {self.lastCacheUpdate - start_time}s.")
|
||||
except Exception:
|
||||
logger.error(f"[CACHE] Error while refreshing cache.\n{traceback.format_exc()}")
|
||||
success = False
|
||||
finally:
|
||||
self.updating.release()
|
||||
|
||||
# Apply new cache if there were no errors
|
||||
if(success):
|
||||
self.cache = cache
|
||||
self.order_list = orderList
|
||||
|
||||
# Validating rooms
|
||||
rooms = list(filter(lambda o: (o.code == o.room_id), self.cache.values()))
|
||||
asyncio.create_task(validate_rooms(None, rooms, self))
|
||||
|
||||
return success
|
||||
|
||||
async def get_order(self, request=None, code=None, secret=None, nfc_id=None, cached=False):
|
||||
|
||||
# if it's a nfc id, just retorn it
|
||||
|
@ -233,6 +431,7 @@ class OrderManager:
|
|||
if order.nfc_id == nfc_id:
|
||||
return order
|
||||
|
||||
await self.update_cache()
|
||||
# If a cached order is needed, just get it if available
|
||||
if code and cached and code in self.cache and time()-self.cache[code].time < 3600:
|
||||
return self.cache[code]
|
||||
|
@ -243,27 +442,26 @@ class OrderManager:
|
|||
secret = request.cookies.get("foxo_secret")
|
||||
|
||||
if re.match('^[A-Z0-9]{5}$', code or '') and (secret is None or re.match('^[a-z0-9]{16,}$', secret)):
|
||||
print('Fetching', code, 'with secret', secret)
|
||||
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'Fetching {code} with secret {secret}')
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
res = await client.get(join(base_url, f"orders/{code}/"), headers=headers)
|
||||
if res.status_code != 200:
|
||||
if request:
|
||||
raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.")
|
||||
else:
|
||||
self.remove_cache(code)
|
||||
return None
|
||||
|
||||
res = res.json()
|
||||
|
||||
order = Order(res)
|
||||
if order.status in ['canceled', 'expired']:
|
||||
self.remove_cache(order.code)
|
||||
if request:
|
||||
raise exceptions.Forbidden(f"Your order has been deleted. Contact support with your order identifier ({res['code']}) for further info.")
|
||||
res = await pretixClient.get(f"orders/{code}/", expectedStatusCodes=None)
|
||||
if res.status_code != 200:
|
||||
if request:
|
||||
raise exceptions.Forbidden("Your session has expired due to order deletion or change! Please check your E-Mail for more info.")
|
||||
else:
|
||||
self.add_cache(order)
|
||||
|
||||
if request and secret != res['secret']:
|
||||
raise exceptions.Forbidden("Your session has expired due to a token change. Please check your E-Mail for an updated link!")
|
||||
return order
|
||||
self.remove_cache(code)
|
||||
return None
|
||||
|
||||
res = res.json()
|
||||
|
||||
order = Order(res)
|
||||
if order.status in ['canceled', 'expired']:
|
||||
self.remove_cache(order.code)
|
||||
if request:
|
||||
raise exceptions.Forbidden(f"Your order has been deleted. Contact support with your order identifier ({res['code']}) for further info.")
|
||||
else:
|
||||
self.add_cache(order)
|
||||
|
||||
if request and secret != res['secret']:
|
||||
raise exceptions.Forbidden("Your session has expired due to a token change. Please check your E-Mail for an updated link!")
|
||||
return order
|
|
@ -0,0 +1,92 @@
|
|||
from config import *
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from sanic import Blueprint, exceptions
|
||||
from sanic.log import logger
|
||||
|
||||
jobs = []
|
||||
|
||||
def draw_profile (source, member, position, font, size=(170, 170), border_width=5):
|
||||
idraw = ImageDraw.Draw(source)
|
||||
source_size = source.size
|
||||
main_fill = (187, 198, 206)
|
||||
propic_x = position[0]
|
||||
propic_y = (source_size[1] // 2) - (size[1] // 2)
|
||||
border_loc = (propic_x, propic_y, propic_x + size[0] + border_width * 2, propic_y + size[1] + border_width *2)
|
||||
profile_location = (propic_x + border_width, propic_y + border_width)
|
||||
propic_name_y = propic_y + size[1] + border_width + 20
|
||||
border_color = SPONSORSHIP_COLOR_MAP[member['sponsorship']] if member['sponsorship'] in SPONSORSHIP_COLOR_MAP.keys() else (84, 110, 122)
|
||||
# Draw border
|
||||
idraw.rounded_rectangle(border_loc, border_width, border_color)
|
||||
# Draw profile picture
|
||||
fileName = member['propic'] or 'default.png'
|
||||
with Image.open(f'res/propic/{fileName}') as to_add:
|
||||
source.paste(to_add.resize (size), profile_location)
|
||||
name_len = idraw.textlength(str(member['name']), font)
|
||||
calc_size = 0
|
||||
if name_len > size[0]:
|
||||
calc_size = size[0] * 20 / name_len if name_len > size[0] else 20
|
||||
font = ImageFont.truetype(font.path, calc_size)
|
||||
name_len = idraw.textlength(str(member['name']), font)
|
||||
name_loc = (position[0] + ((size[0] / 2) - name_len / 2), propic_name_y + (calc_size/2))
|
||||
name_color = SPONSORSHIP_COLOR_MAP[member['sponsorship']] if member['sponsorship'] in SPONSORSHIP_COLOR_MAP.keys() else main_fill
|
||||
idraw.text(name_loc, str(member['name']), font=font, fill=name_color)
|
||||
|
||||
async def generate_room_preview(request, code, room_data):
|
||||
if code in jobs: raise exceptions.SanicException("Please try again later!", status_code=409)
|
||||
font_path = f'res/font/NotoSans-Bold.ttf'
|
||||
main_fill = (187, 198, 206)
|
||||
propic_size = (170, 170)
|
||||
logo_size = (200, 43)
|
||||
border_width = 5
|
||||
propic_gap = 50
|
||||
propic_width = propic_size[0] + (border_width * 2)
|
||||
propic_total_width = propic_width + propic_gap
|
||||
jobs.append(code)
|
||||
try:
|
||||
room_data = await get_room(request, code) if not room_data else room_data
|
||||
if not room_data: return
|
||||
width = max([(propic_width + propic_gap) * int(room_data['capacity']) + propic_gap, 670])
|
||||
height = int(width * 0.525)
|
||||
font = ImageFont.truetype(font_path, 20)
|
||||
|
||||
# Recalculate gap
|
||||
propic_gap = (width - (propic_width * int(room_data['capacity']))) // (int(room_data['capacity']) + 1)
|
||||
propic_total_width = propic_width + propic_gap
|
||||
|
||||
# Define output image
|
||||
with Image.new('RGB', (width, height), (17, 25, 31)) as source:
|
||||
# Draw logo
|
||||
with (Image.open('res/furizon.png') as logo, logo.resize(logo_size).convert('RGBA') as resized_logo):
|
||||
source.paste(resized_logo, ((source.size[0] // 2) - (logo_size[0] // 2), 10), resized_logo)
|
||||
i_draw = ImageDraw.Draw(source)
|
||||
# Draw room's name
|
||||
room_name_len = i_draw.textlength(room_data['name'], font)
|
||||
i_draw.text((((width / 2) - room_name_len / 2), 55), room_data['name'], font=font, fill=main_fill)
|
||||
# Draw members
|
||||
for m in range (room_data['capacity']):
|
||||
member = room_data['members'][m] if m < len(room_data['members']) else { 'name': 'Empty', 'propic': '../new.png', 'sponsorship': None }
|
||||
font = ImageFont.truetype(font_path, 20)
|
||||
draw_profile(source, member, (propic_gap + (propic_total_width * m), 63), font, propic_size, border_width)
|
||||
source.save(f'res/rooms/{code}.jpg', 'JPEG', quality=60)
|
||||
except Exception as err:
|
||||
if EXTRA_PRINTS: logger.exception(str(err))
|
||||
finally:
|
||||
# Remove fault job
|
||||
if len(jobs) > 0: jobs.pop()
|
||||
if not room_data:
|
||||
raise exceptions.SanicException("There's no room with that code.", status_code=404)
|
||||
|
||||
async def get_room (request, code):
|
||||
order_data = await request.app.ctx.om.get_order(code=code)
|
||||
if not order_data or not order_data.room_owner: return None
|
||||
members_map = [{'name': order_data.name, 'propic': order_data.propic, 'sponsorship': order_data.sponsorship}]
|
||||
for member_code in order_data.room_members:
|
||||
if member_code == order_data.code: continue
|
||||
member_order = await request.app.ctx.om.get_order(code=member_code)
|
||||
if not member_order: continue
|
||||
members_map.append ({'name': member_order.name, 'propic': member_order.propic, 'sponsorship': member_order.sponsorship})
|
||||
return {'name': order_data.room_name,
|
||||
'confirmed': order_data.room_confirmed,
|
||||
'capacity': order_data.room_person_no,
|
||||
'free_spots': order_data.room_person_no - len(members_map),
|
||||
'members': members_map}
|
|
@ -10,7 +10,7 @@ bp = Blueprint("karaoke", url_prefix="/manage/karaoke")
|
|||
@bp.get("/admin")
|
||||
async def show_songs(request, order: Order):
|
||||
|
||||
if order.code not in ADMINS:
|
||||
if not order.isAdmin():
|
||||
raise exceptions.Forbidden("Birichino")
|
||||
|
||||
orders = [x for x in request.app.ctx.om.cache.values() if x.karaoke_songs]
|
||||
|
@ -28,7 +28,7 @@ async def show_songs(request, order: Order):
|
|||
@bp.post("/approve")
|
||||
async def approve_songs(request, order: Order):
|
||||
|
||||
if order.code not in ADMINS:
|
||||
if not order.isAdmin():
|
||||
raise exceptions.Forbidden("Birichino")
|
||||
|
||||
for song in request.form:
|
||||
|
@ -44,7 +44,7 @@ async def sing_song(request, order: Order, songname):
|
|||
|
||||
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
||||
if order.code not in ADMINS:
|
||||
if not order.isAdmin():
|
||||
raise exceptions.Forbidden("Birichino")
|
||||
|
||||
songname = unquote(songname)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
ROOM_ERROR_TYPES = {
|
||||
'room_id_mismatch': "There's a member in your room that is actually in another room, too. Please contact us as soon as possible in order to fix this issue.",
|
||||
'unpaid': "Somebody in your room has not paid for their reservation, yet.",
|
||||
'type_mismatch': "A member in your room has a ticket for a different type of room capacity. This happens when users swap their room types with others, without abandoning the room.",
|
||||
'daily': "Some member in your room has a Daily ticket. These tickets do not include a hotel reservation.",
|
||||
'capacity_mismatch': "The number of people in your room mismatches your type of ticket.",
|
||||
'canceled': "Someone in your room canceled his booking and it was removed from your room."
|
||||
}
|
||||
|
||||
EMAILS_TEXT = {
|
||||
"ROOM_UNCONFIRM_TITLE": "Your room got unconfirmed",
|
||||
"ROOM_UNCONFIRM_TEXT": {
|
||||
'html': "Hello <b>{0}</b><br>We had to <b>unconfirm or change</b> your room <i>'{1}'</i> due to the following issues:<br></p>{2}<br><p>Please contact your room's owner or contact our support for further informations at <a href=\"https://furizon.net/contact/\"> https://furizon.net/contact/</a>.<br>Thank you.<br><br><a class=\"link\" style=\"background-color: #1095c1; color: #fff;\" href=\"https://reg.furizon.net/manage/welcome\">Manage booking</a>",
|
||||
|
||||
'plain': "Hello {0}\nWe had to unconfirm or change your room '{1}' due to the following issues:\n{2}\nPlease contact your room's owner or contact our support for further informations at https://furizon.net/contact/.\nThank you\n\nTo manage your booking: https://reg.furizon.net/manage/welcome"
|
||||
},
|
||||
|
||||
|
||||
"MISSING_PROPIC_TITLE": "You haven't uploaded your badges yet!",
|
||||
"MISSING_PROPIC_TEXT": {
|
||||
'html': "Hello <b>{0}</b><br>We noticed you still have to <b>upload {1}</b>!<br>Please enter your booking page at <a href=\"https://reg.furizon.net/manage/welcome\"> https://reg.furizon.net/manage/welcome</a> and <b>upload them</b> under the <i>\"Badge Customization\"</i> section.<br>Thank you.<br><br><a class=\"link\" style=\"background-color: #1095c1; color: #fff;\" href=\"https://reg.furizon.net/manage/welcome\">Manage booking</a>",
|
||||
|
||||
'plain': "Hello {0}\nWe noticed you still have to upload {1}!\nPlease enter your booking page at https://reg.furizon.net/manage/welcome and upload them under the \"Badge Customization\" section.\nThank you."
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
NOSECOUNT = {
|
||||
'filters': {
|
||||
'capacity': "Here are some furs that share your room type and don't have a confirmed room."
|
||||
}
|
||||
}
|
||||
|
||||
LOCALES = {
|
||||
'shuttle_link': {
|
||||
'en': 'Book now',
|
||||
'it': 'Prenota ora'
|
||||
},
|
||||
'shuttle_link_url': {
|
||||
'en': 'https://visitfiemme.regiondo.com/furizon?_ga=2.129644046.307369854.1705325023-235291123.1705325023',
|
||||
'it': 'https://experience.visitfiemme.it/furizon'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
from sanic.log import logger, logging
|
||||
from logging import LogRecord
|
||||
from config import *
|
||||
import traceback
|
||||
|
||||
METRICS_REQ_NO = 0
|
||||
METRICS_ERR_NO = 0 # Errors served to the clients
|
||||
METRICS_PRETIX_READ = 0
|
||||
METRICS_PRETIX_WRITE = 0
|
||||
METRICS_PRETIX_ERRORS = 0 # Errors requesting pretix's backend
|
||||
|
||||
def incPretixRead():
|
||||
global METRICS_PRETIX_READ
|
||||
METRICS_PRETIX_READ += 1
|
||||
|
||||
def incPretixWrite():
|
||||
global METRICS_PRETIX_WRITE
|
||||
METRICS_PRETIX_WRITE += 1
|
||||
|
||||
def incPretixErrors():
|
||||
global METRICS_PRETIX_ERRORS
|
||||
METRICS_PRETIX_ERRORS += 1
|
||||
|
||||
def incReqNo():
|
||||
global METRICS_REQ_NO
|
||||
METRICS_REQ_NO += 1
|
||||
|
||||
def incErrorNo(): # Errors served to the clients
|
||||
global METRICS_ERR_NO
|
||||
METRICS_ERR_NO += 1
|
||||
|
||||
|
||||
def getMetricsText():
|
||||
global METRICS_REQ_NO
|
||||
global METRICS_ERR_NO
|
||||
global METRICS_PRETIX_READ
|
||||
global METRICS_PRETIX_WRITE
|
||||
global METRICS_PRETIX_ERRORS
|
||||
out = []
|
||||
|
||||
out.append(f'sanic_request_count{{}} {METRICS_REQ_NO}')
|
||||
out.append(f'sanic_error_count{{}} {METRICS_ERR_NO}')
|
||||
out.append(f'webint_pretix_read_count{{}} {METRICS_PRETIX_READ}')
|
||||
out.append(f'webint_pretix_write_count{{}} {METRICS_PRETIX_WRITE}')
|
||||
out.append(f'webint_pretix_error_count{{}} {METRICS_PRETIX_ERRORS}')
|
||||
|
||||
return "\n".join(out)
|
||||
|
||||
def getRoomCountersText(request):
|
||||
out = []
|
||||
try:
|
||||
daily = 0
|
||||
counters = {}
|
||||
counters_early = {}
|
||||
counters_late = {}
|
||||
for id in ROOM_TYPE_NAMES.keys():
|
||||
counters[id] = 0
|
||||
counters_early[id] = 0
|
||||
counters_late[id] = 0
|
||||
|
||||
for order in request.app.ctx.om.cache.values():
|
||||
if(order.daily):
|
||||
daily += 1
|
||||
else:
|
||||
# Order.status must reflect the one in the Order() constructor inside ext.py
|
||||
if(order.status in ["pending", "paid"] and hasattr(order, "bed_in_room") and order.bed_in_room in counters):
|
||||
counters[order.bed_in_room] += 1
|
||||
if(order.has_early):
|
||||
counters_early[order.bed_in_room] += 1
|
||||
if(order.has_late):
|
||||
counters_late[order.bed_in_room] += 1
|
||||
|
||||
for id, count in counters.items():
|
||||
out.append(f'webint_order_room_counter{{days="normal", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
|
||||
for id, count in counters_early.items():
|
||||
out.append(f'webint_order_room_counter{{days="early", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
|
||||
for id, count in counters_late.items():
|
||||
out.append(f'webint_order_room_counter{{days="late", label="{ROOM_TYPE_NAMES[id]}"}} {count}')
|
||||
out.append(f'webint_order_room_counter{{label="Daily"}} {daily}')
|
||||
|
||||
except Exception as e:
|
||||
print(traceback.format_exc())
|
||||
|
||||
logger.warning("Error in loading metrics rooms")
|
||||
return "\n".join(out)
|
||||
|
||||
class MetricsFilter(logging.Filter):
|
||||
def filter(self, record : LogRecord):
|
||||
return not (record.request.endswith("/manage/metrics") and record.status == 200)
|
|
@ -0,0 +1,50 @@
|
|||
import httpx
|
||||
from utils import *
|
||||
from config import *
|
||||
from sanic.log import logger
|
||||
from metrics import *
|
||||
import traceback
|
||||
import asyncio
|
||||
|
||||
async def get(url, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
|
||||
async def func(client : httpx.AsyncClient) -> httpx.Request:
|
||||
return await client.get(join(baseUrl, url), headers=headers)
|
||||
return await doReq(url, func, incPretixRead, expectedStatusCodes, "GETing")
|
||||
|
||||
async def post(url, content=None, json=None, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
|
||||
async def func(client : httpx.AsyncClient) -> httpx.Request:
|
||||
return await client.post(join(baseUrl, url), headers=headers, content=content, json=json)
|
||||
return await doReq(url, func, incPretixWrite, expectedStatusCodes, "POSTing")
|
||||
|
||||
async def patch(url, json, baseUrl=base_url_event, headers=headers, expectedStatusCodes=[200]) -> httpx.Response:
|
||||
async def func(client : httpx.AsyncClient) -> httpx.Request:
|
||||
return await client.patch(join(baseUrl, url), headers=headers, json=json)
|
||||
return await doReq(url, func, incPretixWrite, expectedStatusCodes, "PATCHing")
|
||||
|
||||
|
||||
|
||||
async def doReq(url, httpxFunc, metricsFunc, expectedStatusCodes, opLogString) -> httpx.Response:
|
||||
res = None
|
||||
async with httpx.AsyncClient(timeout=PRETIX_REQUESTS_TIMEOUT) as client:
|
||||
requests = 0
|
||||
for requests in range(PRETIX_REQUESTS_MAX):
|
||||
try:
|
||||
metricsFunc()
|
||||
res : httpx.Response = await httpxFunc(client)
|
||||
|
||||
if expectedStatusCodes is not None and res.status_code not in expectedStatusCodes:
|
||||
incPretixErrors()
|
||||
logger.warning(f"[PRETIX] Got an unexpected status code ({res.status_code}) while {opLogString} '{url}'. Allowed status codes: {', '.join(map(str, expectedStatusCodes))}")
|
||||
logger.debug(f"Response: '{res.text}'")
|
||||
continue
|
||||
break
|
||||
except Exception as e:
|
||||
incPretixErrors()
|
||||
logger.warning(f"[PRETIX] An error ({requests}) occurred while {opLogString} '{url}':\n{traceback.format_exc()}")
|
||||
|
||||
requests += 1
|
||||
else:
|
||||
logger.error(f"[PRETIX] Reached PRETIX_REQUESTS_MAX ({PRETIX_REQUESTS_MAX}) while {opLogString} '{url}'. Aborting")
|
||||
raise httpx.TimeoutException(f"PRETIX_REQUESTS_MAX reached while {opLogString} to pretix.")
|
||||
|
||||
return res
|
79
propic.py
|
@ -6,9 +6,31 @@ from PIL import Image
|
|||
from io import BytesIO
|
||||
from hashlib import sha224
|
||||
from time import time
|
||||
import os
|
||||
|
||||
bp = Blueprint("propic", url_prefix="/manage/propic")
|
||||
|
||||
async def resetDefaultPropic(request, order: Order, isFursuiter, sendAnswer=True):
|
||||
s = "_fursuiter" if isFursuiter else ""
|
||||
if (EXTRA_PRINTS):
|
||||
logger.info("Resetting {fn} picture for {orderCod}".format(fn="Badge" if not isFursuiter else "fursuit", orderCod = order.code))
|
||||
with open("res/propic/default.png", "rb") as f:
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
convertedFilename = order.ans(f'propic{s}')
|
||||
if convertedFilename is not None:
|
||||
convertedFilename = f"res/propic/{convertedFilename}"
|
||||
if os.path.exists(convertedFilename):
|
||||
os.remove(convertedFilename) # converted file
|
||||
originalFilename = f"res/propic/propic{s}_{order.code}_original"
|
||||
if os.path.exists(originalFilename):
|
||||
os.remove(originalFilename) # original file
|
||||
|
||||
await order.edit_answer_fileUpload(f'propic{s}_file', f'propic{s}_file_{order.code}_default.png', 'image/png', data)
|
||||
if(sendAnswer):
|
||||
await order.send_answers()
|
||||
|
||||
@bp.post("/upload")
|
||||
async def upload_propic(request, order: Order):
|
||||
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
@ -20,9 +42,11 @@ async def upload_propic(request, order: Order):
|
|||
raise exceptions.BadRequest("The deadline has passed. You cannot modify the badges at this moment.")
|
||||
|
||||
if request.form.get('submit') == 'Delete main image':
|
||||
await order.edit_answer('propic', None)
|
||||
await resetDefaultPropic(request, order, False, sendAnswer=False)
|
||||
await order.edit_answer('propic', None) #This MUST come after the reset default propic!
|
||||
elif request.form.get('submit') == 'Delete fursuit image':
|
||||
await order.edit_answer('propic_fursuiter', None)
|
||||
await resetDefaultPropic(request, order, True, sendAnswer=False)
|
||||
await order.edit_answer('propic_fursuiter', None) #This MUST come after the reset default propic!
|
||||
else:
|
||||
for fn, body in request.files.items():
|
||||
if fn not in ['propic', 'propic_fursuiter']:
|
||||
|
@ -30,15 +54,33 @@ async def upload_propic(request, order: Order):
|
|||
|
||||
if not body[0].body: continue
|
||||
|
||||
h = sha224(body[0].body).hexdigest()[:32]
|
||||
# Check max file size
|
||||
if EXTRA_PRINTS:
|
||||
logger.debug (f"Image {fn} weight: {len(body[0].body)} bytes")
|
||||
if len(body[0].body) > PROPIC_MAX_FILE_SIZE:
|
||||
raise exceptions.BadRequest("File size too large for " + ("Profile picture" if fn == 'propic' else 'Fursuit picture'))
|
||||
|
||||
errorDetails = ''
|
||||
bodyBytesBuff = None
|
||||
imgBytesBuff = None
|
||||
img = None
|
||||
try:
|
||||
img = Image.open(BytesIO(body[0].body))
|
||||
bodyBytesBuff = BytesIO(body[0].body)
|
||||
img = Image.open(bodyBytesBuff)
|
||||
width, height = img.size
|
||||
# Checking for min / max size
|
||||
if width < PROPIC_MIN_SIZE[0] or height < PROPIC_MIN_SIZE[1]:
|
||||
errorDetails = "Image too small [{width}x{width}] for {pfpn}".format(width=width, pfpn=("Profile picture" if fn == 'propic' else 'Fursuit picture'))
|
||||
raise exceptions.BadRequest(errorDetails)
|
||||
|
||||
if width > PROPIC_MAX_SIZE[0] or height > PROPIC_MAX_SIZE[1]:
|
||||
errorDetails = "Image too big [{width}x{width}] for {pfpn}".format(width=width, pfpn=("Profile picture" if fn == 'propic' else 'Fursuit picture'))
|
||||
raise exceptions.BadRequest(errorDetails)
|
||||
|
||||
|
||||
with open(f"res/propic/{fn}_{order.code}_original", "wb") as f:
|
||||
f.write(body[0].body)
|
||||
|
||||
width, height = img.size
|
||||
aspect_ratio = width/height
|
||||
if aspect_ratio > 1:
|
||||
crop_amount = (width - height) / 2
|
||||
|
@ -48,12 +90,31 @@ async def upload_propic(request, order: Order):
|
|||
img = img.crop((0, crop_amount, width, height - crop_amount))
|
||||
|
||||
img = img.convert('RGB')
|
||||
width, height = img.size
|
||||
|
||||
img.thumbnail((512,512))
|
||||
img.save(f"res/propic/{fn}_{order.code}_{h}.jpg")
|
||||
except:
|
||||
raise exceptions.BadRequest("The image you uploaded is not valid.")
|
||||
imgBytesBuff = BytesIO()
|
||||
img.save(imgBytesBuff, format='jpeg')
|
||||
imgBytes = imgBytesBuff.getvalue()
|
||||
with open(f"res/propic/{fn}_{order.code}.jpg", "wb") as f:
|
||||
f.write(imgBytes)
|
||||
|
||||
await order.edit_answer_fileUpload(f'{fn}_file', f'{fn}_file_{order.code}.jpg', 'image/jpeg', imgBytes)
|
||||
except Exception:
|
||||
import traceback
|
||||
if EXTRA_PRINTS: print(traceback.format_exc())
|
||||
raise exceptions.BadRequest(errorDetails if errorDetails else "The image you uploaded is not valid.")
|
||||
else:
|
||||
await order.edit_answer(fn, f"{fn}_{order.code}_{h}.jpg")
|
||||
await order.edit_answer(fn, f"{fn}_{order.code}.jpg")
|
||||
|
||||
if img is not None:
|
||||
img.close()
|
||||
if bodyBytesBuff is not None:
|
||||
bodyBytesBuff.flush()
|
||||
bodyBytesBuff.close()
|
||||
if imgBytesBuff is not None:
|
||||
imgBytesBuff.flush()
|
||||
imgBytesBuff.close()
|
||||
|
||||
await order.send_answers()
|
||||
return redirect("/manage/welcome#badge")
|
||||
|
|
After Width: | Height: | Size: 78 MiB |
After Width: | Height: | Size: 292 KiB |
After Width: | Height: | Size: 208 KiB |
|
@ -0,0 +1,21 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Thanks!</title>
|
||||
<style>
|
||||
body {font-family: sans-serif;color:#eee;background:#222;}
|
||||
main {margin: 4em auto;max-width:50em;line-height:2em;}
|
||||
h1, h2 {color:#e90;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1 id="a">Thanks for participating in Furizon ENABLE_JAVASCRIPT!</h1>
|
||||
<p>We just came back home and we need some time to regain energy and get back to normal! The registration system is currently offline, and we are working on making it better!</p>
|
||||
<h2>When will it be back online?</h2>
|
||||
<p>The registration system was hosted in a server in the convention network itself. After the end of the convention, the network was disassembled. We will soon setup the server again!</p>
|
||||
<h2>Can i contact you?</h2>
|
||||
<p>Yes. Just write to info@furizon.net</p>
|
||||
</main>
|
||||
</body>
|
||||
<script>document.getElementById("a").innerText = "Thanks for participating in Furizon " + (new Date().getFullYear()) + "!"</script>
|
||||
</html>
|
|
@ -0,0 +1,147 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Countdown to Furizon</title>
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<meta name="viewport" content="width=500rem" />
|
||||
<link href="https://fonts.bunny.net/css?family=pt-serif-caption:400" rel="stylesheet" />
|
||||
<meta property="og:title" content="Furizon Overlord · Booking">
|
||||
<meta property="og:description" content="Acquista i biglietti per Furizon Overlord, la convention furry Italiana.">
|
||||
<style>
|
||||
|
||||
*{margin:0;border:0;padding:0;}
|
||||
|
||||
body {
|
||||
font-family: 'PT Serif Caption';
|
||||
background: url('bg.jpg') center center no-repeat;
|
||||
backdrop-filter: blur(5px);
|
||||
background-color:#111;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: calc(100vh * 1.406)) { /*1.406 is width/height of the image*/
|
||||
.containedBg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('bg.jpg') center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: calc(100vh * 1.406)) and (min-width: calc(100vh * 0.706)) {
|
||||
.containedBg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('bg.jpg') right center no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: calc(100vh * 0.706)) {
|
||||
.containedBg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('bgVert.jpg') center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
img {
|
||||
display:block;
|
||||
height: 6rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1em 0;
|
||||
font-size: 1.2rem;
|
||||
color: #fff;
|
||||
width:100%;
|
||||
box-sizing:border-box;
|
||||
background:rgba(0,0,0,0.5);
|
||||
text-align:center;
|
||||
position:absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
em {
|
||||
color: #FCB53F;
|
||||
font-size: 0.5em;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
main a {
|
||||
font-size:1.6em;
|
||||
margin-top: 0.5em;
|
||||
color: #FCB53F;
|
||||
background:#000;
|
||||
border-radius: 5px;
|
||||
text-decoration:none;
|
||||
padding: 0.3em 0.6em;
|
||||
display:inline-block;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
main a:hover {
|
||||
background:#FCB53F;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
#timer {display:block;max-width:30em;margin: 0 auto;}
|
||||
#clock {display:block;}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="containedBg"></div>
|
||||
<main>
|
||||
<div id="timer">
|
||||
<img src="https://furizon.net/wp-content/uploads/2022/08/Logo_base_no_sfondoo_Furizon_1-1.png" />
|
||||
<h2>Overlord Registration</h2>
|
||||
<span id="clock">Enjoy furizon~</span>
|
||||
<a id="button" href="https://reg.furizon.net/furizon/overlord/" style="display:none">Book now!</a>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
// Set the date we're counting down to
|
||||
var countDownDate = 1705143600 * 1000;
|
||||
|
||||
var now = new Date().getTime();
|
||||
if(now <= countDownDate) {
|
||||
// Update the count down every 1 second
|
||||
var x = setInterval(function() {
|
||||
|
||||
// Get today's date and time
|
||||
var now = new Date().getTime();
|
||||
|
||||
// Find the distance between now and the count down date
|
||||
var distance = countDownDate - now;
|
||||
|
||||
// Time calculations for days, hours, minutes and seconds
|
||||
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
|
||||
// Display the result in the element with id="timer"
|
||||
document.getElementById("clock").innerHTML = days + "<em>d</em> " + hours + "<em>h</em> "
|
||||
+ minutes + "<em>m</em> " + seconds + "<em>s</em>";
|
||||
|
||||
// If the count down is finished, write some text
|
||||
if (distance < 0) {
|
||||
clearInterval(x);
|
||||
document.getElementById("button").style.display = 'block';
|
||||
document.getElementById("clock").style.display = 'none';
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
document.getElementById("button").style.display = 'block';
|
||||
document.getElementById("clock").style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Countdown to Furizon</title>
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<meta name="viewport" content="width=500rem" />
|
||||
<link href="https://fonts.bunny.net/css?family=pt-serif-caption:400" rel="stylesheet" />
|
||||
<meta property="og:title" content="Furizon Beyond · Booking">
|
||||
<meta property="og:description" content="Acquista i biglietti per Furizon Beyond, la convention furry Italiana.">
|
||||
<style>
|
||||
|
||||
*{margin:0;border:0;padding:0;}
|
||||
|
||||
body {
|
||||
font-family: 'PT Serif Caption';
|
||||
background-image: url('https://furizon.net/wp-content/uploads/2022/12/Header-banner.jpg?id=1635');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right;
|
||||
background-size: cover;
|
||||
background-color:#111;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
img {
|
||||
display:block;
|
||||
height: 6rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1em 0;
|
||||
font-size: 1.2rem;
|
||||
color: #fff;
|
||||
width:100%;
|
||||
box-sizing:border-box;
|
||||
background:rgba(0,0,0,0.5);
|
||||
text-align:center;
|
||||
position:absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
em {
|
||||
color: #FCB53F;
|
||||
font-size: 0.5em;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
main a {
|
||||
font-size:1.6em;
|
||||
margin-top: 0.5em;
|
||||
color: #FCB53F;
|
||||
background:#000;
|
||||
border-radius: 5px;
|
||||
text-decoration:none;
|
||||
padding: 0.3em 0.6em;
|
||||
display:inline-block;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
main a:hover {
|
||||
background:#FCB53F;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
#timer {display:block;max-width:30em;margin: 0 auto;}
|
||||
#clock {display:block;}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div id="timer">
|
||||
<img src="https://furizon.net/wp-content/uploads/2022/08/Logo_base_no_sfondoo_Furizon_1-1.png" />
|
||||
<h2>Thank you to everybody who registered for Furizon 2023!</h2>
|
||||
<p>Too late to register? You can still sign up to the waiting list! We will contact you as soon as a spot is available for you~</p>
|
||||
<!--<span id="clock">Enjoy furizon~</span>-->
|
||||
<a id="button" href="https://reg.furizon.net/furizon/beyond/waitinglist?item=38">Join waiting list</a>
|
||||
<a id="button" href="https://reg.furizon.net/furizon/beyond/redeem">Redeem code</a>
|
||||
</div>
|
||||
</main>
|
||||
<!--<script>
|
||||
// Set the date we're counting down to
|
||||
var countDownDate = 1685455200 * 1000;
|
||||
|
||||
var now = new Date().getTime();
|
||||
if(now <= countDownDate) {
|
||||
// Update the count down every 1 second
|
||||
var x = setInterval(function() {
|
||||
|
||||
// Get today's date and time
|
||||
var now = new Date().getTime();
|
||||
|
||||
// Find the distance between now and the count down date
|
||||
var distance = countDownDate - now;
|
||||
|
||||
// Time calculations for days, hours, minutes and seconds
|
||||
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
|
||||
// Display the result in the element with id="timer"
|
||||
document.getElementById("clock").innerHTML = days + "<em>d</em> " + hours + "<em>h</em> "
|
||||
+ minutes + "<em>m</em> " + seconds + "<em>s</em>";
|
||||
|
||||
// If the count down is finished, write some text
|
||||
if (distance < 0) {
|
||||
clearInterval(x);
|
||||
document.getElementById("clock").style.display = 'none';
|
||||
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
</script>-->
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 280 KiB |
|
@ -3,4 +3,5 @@ sanic-ext
|
|||
httpx
|
||||
Pillow
|
||||
aztec_code_generator
|
||||
jinja2
|
||||
Jinja2
|
||||
Requests
|
|
@ -94,13 +94,13 @@ function clock() {
|
|||
currentTime = new Date();
|
||||
let ts = currentTime.toString();
|
||||
ts = ts.replace("GMT+0200 (Central European Summer Time)", "");
|
||||
ts = ts.replace("2023", "<br />");
|
||||
ts = ts.replace("2024", "<br />");
|
||||
document.getElementById("clock").innerHTML = ts;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function fastForward() {
|
||||
currentTime = new Date("2023-05-29T18:00Z");
|
||||
currentTime = new Date("2024-06-03T18:00Z");
|
||||
setInterval(() => {
|
||||
updateDivs(cachedData);
|
||||
currentTime.setMinutes(currentTime.getMinutes()+1);
|
||||
|
|
BIN
res/botbg2.png
Before Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 204 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 19C13 20.1 13.3 21.12 13.81 22H6C4.89 22 4 21.11 4 20V4C4 2.9 4.89 2 6 2H7V9L9.5 7.5L12 9V2H18C19.1 2 20 2.89 20 4V13.09C19.67 13.04 19.34 13 19 13C15.69 13 13 15.69 13 19M20 18V15H18V18H15V20H18V23H20V20H23V18H20Z" /></svg>
|
After Width: | Height: | Size: 297 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18,11H6V6H18M16.5,17A1.5,1.5 0 0,1 15,15.5A1.5,1.5 0 0,1 16.5,14A1.5,1.5 0 0,1 18,15.5A1.5,1.5 0 0,1 16.5,17M7.5,17A1.5,1.5 0 0,1 6,15.5A1.5,1.5 0 0,1 7.5,14A1.5,1.5 0 0,1 9,15.5A1.5,1.5 0 0,1 7.5,17M4,16C4,16.88 4.39,17.67 5,18.22V20A1,1 0 0,0 6,21H7A1,1 0 0,0 8,20V19H16V20A1,1 0 0,0 17,21H18A1,1 0 0,0 19,20V18.22C19.61,17.67 20,16.88 20,16V6C20,2.5 16.42,2 12,2C7.58,2 4,2.5 4,6V16Z" /></svg>
|
After Width: | Height: | Size: 466 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M580-240q-42 0-71-29t-29-71q0-42 29-71t71-29q42 0 71 29t29 71q0 42-29 71t-71 29ZM200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z"/></svg>
|
After Width: | Height: | Size: 386 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" /></svg>
|
After Width: | Height: | Size: 188 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>
|
After Width: | Height: | Size: 300 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M440-440q17 0 28.5-11.5T480-480q0-17-11.5-28.5T440-520q-17 0-28.5 11.5T400-480q0 17 11.5 28.5T440-440ZM280-120v-80l240-40v-445q0-15-9-27t-23-14l-208-34v-80l220 36q44 8 72 41t28 77v512l-320 54Zm-160 0v-80h80v-560q0-34 23.5-57t56.5-23h400q34 0 57 23t23 57v560h80v80H120Zm160-80h400v-560H280v560Z"/></svg>
|
After Width: | Height: | Size: 399 B |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /></svg>
|
After Width: | Height: | Size: 125 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/></svg>
|
After Width: | Height: | Size: 310 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19A2.92,2.92 0 0,0 18,16.08Z" /></svg>
|
After Width: | Height: | Size: 521 B |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.1 KiB |
|
@ -0,0 +1,21 @@
|
|||
function confirmAction (intent, sender) {
|
||||
if (['propicReminder'].includes (intent) == false) return
|
||||
let href = sender.getAttribute('action')
|
||||
let intentTitle = document.querySelector("#intentText")
|
||||
let intentDescription = document.querySelector("#intentDescription")
|
||||
let intentEditPanel = document.querySelector("#intentEditPanel")
|
||||
let intentFormAction = document.querySelector("#intentFormAction")
|
||||
let intentSend = document.querySelector("#intentSend")
|
||||
// Resetting ui
|
||||
intentFormAction.setAttribute('method', 'GET')
|
||||
intentEditPanel.style.display = 'none';
|
||||
intentDescription.innerText = sender.title;
|
||||
intentFormAction.setAttribute('action', href)
|
||||
switch (intent){
|
||||
case 'propicReminder':
|
||||
intentTitle.innerText = "Send missing badge reminders";
|
||||
intentSend.innerText = sender.innerText;
|
||||
break;
|
||||
}
|
||||
document.getElementById('modalRoomconfirm').setAttribute('open', 'true');
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
function onScrollNav () {
|
||||
if (Number(window.currentScroll) == 0) {
|
||||
window.currentScroll = window.scrollY || document.documentElement.scrollTop;
|
||||
return;
|
||||
}
|
||||
let newOffset = window.scrollY || document.documentElement.scrollTop;
|
||||
|
||||
document.getElementById('topbar').classList.toggle('closed', newOffset > window.currentScroll);
|
||||
window.currentScroll = newOffset <= 0 ? 0 : newOffset;
|
||||
}
|
||||
|
||||
document.getElementById('mobileMenu').addEventListener('click', function (e) {
|
||||
menuClick (e.target);
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
function menuClick (e, force){
|
||||
let menuItem = e.closest('img');
|
||||
let isOpen = false;
|
||||
isOpen = document.querySelector('nav div.navbar-container').classList.toggle('open', force);
|
||||
|
||||
menuItem.setAttribute('src', isOpen ? '/res/icons/close.svg' : '/res/icons/menu.svg');
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
function confirmAction (intent, sender) {
|
||||
if (['rename', 'unconfirm', 'delete'].includes (intent) == false) return
|
||||
let href = sender.getAttribute('action')
|
||||
let intentTitle = document.querySelector("#modalOrderEditDialog #intentText")
|
||||
let intentEdit = document.querySelector("#modalOrderEditDialog #intentRename")
|
||||
let intentEditPanel = document.querySelector("#modalOrderEditDialog #intentEditPanel")
|
||||
let intentFormAction = document.querySelector("#intentFormAction")
|
||||
let intentSend = document.querySelector("#modalOrderEditDialog #intentSend")
|
||||
// Resetting ui
|
||||
intentEdit.removeAttribute('required')
|
||||
intentEdit.removeAttribute('minlength')
|
||||
intentFormAction.setAttribute('method', 'GET')
|
||||
intentEditPanel.style.display = 'none';
|
||||
|
||||
intentTitle.innerText = intent + ' room'
|
||||
intentFormAction.setAttribute('action', href)
|
||||
switch (intent){
|
||||
case 'rename':
|
||||
intentEditPanel.style.display = 'block';
|
||||
intentEdit.setAttribute('required', true)
|
||||
intentEdit.setAttribute('minlength', 4)
|
||||
intentFormAction.setAttribute('method', 'POST')
|
||||
document.getElementById("intentRename").value = sender.parentElement.parentElement.querySelector("span").innerText;
|
||||
break
|
||||
case 'unconfirm':
|
||||
break
|
||||
case 'delete':
|
||||
break
|
||||
}
|
||||
document.getElementById('modalOrderEditDialog').setAttribute('open', 'true');
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
var draggingData = {
|
||||
id: 0,
|
||||
roomTypeId: 0,
|
||||
parentRoomId: 0
|
||||
}
|
||||
|
||||
var allowRedirect = false;
|
||||
|
||||
function initObjects (){
|
||||
draggables = document.querySelectorAll("div.grid.people div.edit-drag");
|
||||
rooms = document.querySelectorAll("main.container>div.room");
|
||||
Array.from(draggables).forEach(element => {
|
||||
element.addEventListener('dragstart', dragStart);
|
||||
element.addEventListener('dragend', dragEnd);
|
||||
});
|
||||
Array.from(rooms).forEach(room => {
|
||||
room.addEventListener('dragenter', dragEnter)
|
||||
room.addEventListener('dragover', dragOver);
|
||||
room.addEventListener('dragleave', dragLeave);
|
||||
room.addEventListener('drop', drop);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DragEvent} e
|
||||
*/
|
||||
function dragStart(e) {
|
||||
element = e.target;
|
||||
room = element.closest('div.room')
|
||||
setData(element.id, element.getAttribute('room-type'), room.id)
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
setTimeout(()=>toggleRoomSelection(true), 0);
|
||||
}
|
||||
|
||||
function dragEnd(e) {
|
||||
toggleRoomSelection(false);
|
||||
resetData ();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function dragEnter(e) {
|
||||
e.preventDefault();
|
||||
e.target.classList.add('drag-over');
|
||||
checkDragLocation (getData(), e.target);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function dragOver(e) {
|
||||
e.preventDefault();
|
||||
e.target.classList.add('drag-over');
|
||||
checkDragLocation (getData(), e.target)
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element} target
|
||||
*/
|
||||
function checkDragLocation (data, target) {
|
||||
let toReturn = true;
|
||||
const isInfinite = target.getAttribute("infinite");
|
||||
const maxSizeReached = target.getAttribute("current-size") >= target.getAttribute("room-size");
|
||||
const roomTypeMismatch = data.roomTypeId !== target.getAttribute("room-type");
|
||||
if (!isInfinite && (maxSizeReached || roomTypeMismatch)) {
|
||||
target.classList.add('drag-forbidden');
|
||||
toReturn = false;
|
||||
} else {
|
||||
target.classList.remove('drag-forbidden');
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
function dragLeave(e) {
|
||||
e.target.classList.remove('drag-over');
|
||||
e.target.classList.remove('drag-forbidden');
|
||||
}
|
||||
|
||||
function drop(e) {
|
||||
e.target.classList.remove('drag-over');
|
||||
toggleRoomSelection(false);
|
||||
if (checkDragLocation(getData(), e.target) === true) {
|
||||
const data = getData();
|
||||
let item = document.getElementById (data.id)
|
||||
let oldParent = document.getElementById (data.parentRoomId)
|
||||
let newParent = e.target;
|
||||
if (moveToRoom (data.id, data.parentRoomId.replace('room-',''), newParent.id.replace('room-','')) === true) {
|
||||
let newParentContainer = newParent.querySelector('div.grid.people')
|
||||
newParentContainer.appendChild (item);
|
||||
let oldParentQty = parseInt(oldParent.getAttribute("current-size")) - 1;
|
||||
let newParentQty = parseInt(newParent.getAttribute("current-size")) + 1;
|
||||
let newParentCapacity = parseInt(newParent.getAttribute("room-size"));
|
||||
oldParent.setAttribute("current-size", oldParentQty);
|
||||
newParent.setAttribute("current-size", newParentQty);
|
||||
oldParent.classList.remove('complete');
|
||||
if (newParentCapacity == newParentQty) newParent.classList.add('complete');
|
||||
// if owner of room is being moved, assign a new owner
|
||||
if (data.parentRoomId.replace('room-','') == data.id) {
|
||||
// find first owner
|
||||
if (model[data.id][toAdd] && model[data.id][toAdd].length <= 0) return;
|
||||
newOwner = model[data.id][toAdd][0]
|
||||
changeOwner (data.id, newOwner)
|
||||
oldParent.id = "room-" + newOwner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRoomSelection(newStatus) {
|
||||
rooms = document.querySelectorAll("div.room");
|
||||
Array.from(rooms).forEach(room=>{
|
||||
room.classList.toggle('interactless', newStatus);
|
||||
room.classList.remove('drag-over');
|
||||
room.classList.remove('drag-forbidden');
|
||||
})
|
||||
}
|
||||
|
||||
function setData (id, roomType, parentRoomId) {
|
||||
draggingData.id = id;
|
||||
draggingData.roomTypeId = roomType;
|
||||
draggingData.parentRoomId = parentRoomId;
|
||||
}
|
||||
|
||||
function resetData (){ setData(0, 0, 0); }
|
||||
|
||||
function getData () { return draggingData; }
|
||||
|
||||
// This default onbeforeunload event
|
||||
window.onbeforeunload = function(){
|
||||
if (!allowRedirect) return "Any changes to the rooms will be discarded."
|
||||
}
|
||||
|
||||
/* Model managing */
|
||||
|
||||
var model = saveData;
|
||||
|
||||
const toAdd = "to_add";
|
||||
|
||||
function moveToRoom (order, from, to){
|
||||
if (!model) { console.error("Model is null", order, from, to); return false; }
|
||||
if (!model[from]) { console.error("Parent is null", order, from, to); return false; }
|
||||
if (!model[to]) { console.error("Destination is null", order, from, to); return false; }
|
||||
if (!model[from][toAdd] || !model[from][toAdd].includes(order)) { console.error("Order is not in parent", order, from, to); return false; }
|
||||
if (!model[to][toAdd]) model[to][toAdd] = [];
|
||||
// Delete order from the original room
|
||||
model[from][toAdd] = model[from][toAdd].filter (itm=> itm !== order)
|
||||
// Add it to the destination room
|
||||
model[to][toAdd].push (order);
|
||||
return true;
|
||||
}
|
||||
|
||||
function changeOwner (from, to){
|
||||
if (!model) { console.error("Model is null", from, to); return false; }
|
||||
if (!model[from]) { console.error("Parent is null", from, to); return false; }
|
||||
if (model[to]) { console.error("Destination already exist", from, to); return false; }
|
||||
model[to] = {...model[from]}
|
||||
delete model[from]
|
||||
}
|
||||
|
||||
function onSave (){
|
||||
if (model['infinite'] && model['infinite'][toAdd] && model['infinite'][toAdd].length > 0) {
|
||||
setTimeout(()=>{
|
||||
let roomItem = document.querySelector("#room-infinite");
|
||||
roomItem.scrollIntoView();
|
||||
roomItem.classList.add('drag-forbidden');
|
||||
setTimeout(()=>roomItem.classList.remove('drag-forbidden'), 3000);
|
||||
}, 100);
|
||||
} else {
|
||||
document.getElementById('modalConfirmDialog').setAttribute('open', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element} element
|
||||
*/
|
||||
function submitData (element){
|
||||
if (element.ariaDisabled) return;
|
||||
element.ariaDisabled = true;
|
||||
element.setAttribute("aria-busy", true);
|
||||
document.querySelector("#modalClose").setAttribute("disabled", true);
|
||||
document.querySelector("#modalClose").style.display = 'none';
|
||||
// Create request
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/manage/admin/room/wizard/submit', true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
let popoverText = document.querySelector("#popover-status-text");
|
||||
let popoverStatus = document.querySelector("#popover-status");
|
||||
popoverStatus.classList.remove('status-error');
|
||||
popoverStatus.classList.remove('status-success');
|
||||
if (xhr.status === 200) {
|
||||
// Handle correct redirect
|
||||
popoverText.innerText = "Changes applied successfully. Redirecting..."
|
||||
popoverStatus.classList.add('status-success');
|
||||
} else {
|
||||
// Handle errors
|
||||
let error = xhr.statusText;
|
||||
popoverText.innerText = "Could not apply changes: " + error;
|
||||
console.error('Error submitting data:', error);
|
||||
popoverStatus.classList.add('status-error');
|
||||
}
|
||||
popoverStatus.showPopover();
|
||||
allowRedirect = true;
|
||||
setTimeout(()=>window.location.assign('/manage/admin'), 3000);
|
||||
}
|
||||
};
|
||||
xhr.send(JSON.stringify(model));
|
||||
}
|
||||
|
||||
initObjects ();
|
|
@ -0,0 +1,53 @@
|
|||
div.room-actions {
|
||||
container-name: room-actions;
|
||||
float: right;
|
||||
}
|
||||
|
||||
div.admin-actions-header {
|
||||
container-name: room-actions;
|
||||
float: unset !important;
|
||||
max-height: 2rem;
|
||||
margin: 1rem 0px;
|
||||
}
|
||||
|
||||
div.admin-actions-header img {
|
||||
max-height: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
div.room-actions > a {
|
||||
background-color: var(--card-background-color);
|
||||
font-size: 12pt;
|
||||
padding: 0.7rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
div.room-actions > a:hover {
|
||||
background-color: var(--primary-focus);
|
||||
}
|
||||
|
||||
div.room-actions > a.act-del:hover {
|
||||
background-color: var(--del-color);
|
||||
}
|
||||
|
||||
/* Spinning animation */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform:rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform:rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
h3:has(.spin) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation-name: spin;
|
||||
animation-duration: 500ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
max-height: 32px;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/* Other blocks' styles */
|
||||
@import url('propic.css');
|
||||
@import url('admin.css');
|
||||
@import url('room.css');
|
||||
@import url('navbar.css');
|
||||
|
||||
a[role=button] {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
summary:has(span.status) {
|
||||
background-color: #ffaf0377;
|
||||
}
|
||||
|
||||
.rainbow-text {
|
||||
background-image: repeating-linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff);
|
||||
background-size: 2000% 2000%;
|
||||
color: transparent;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: rainbow 4s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% { background-position:0% 0%; }
|
||||
100% { background-position:57.75% 0%; }
|
||||
}
|
||||
|
||||
/* Popover */
|
||||
*[popover]:popover-open {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid #fff;
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
@media only screen and (prefers-color-scheme: dark) {
|
||||
.icon {filter: invert(1);}
|
||||
}
|
||||
|
||||
/* Mobile only */
|
||||
@media only screen and ((max-width: 500px) or (hover: none)) {
|
||||
.propic-border.blurred {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (prefers-reduced-motion) {
|
||||
.propic-border.blurred {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
nav#topbar.closed {
|
||||
top: -10rem;
|
||||
}
|
||||
|
||||
nav#topbar {
|
||||
background-color :var(--card-background-color);
|
||||
width: 100vw;
|
||||
position: sticky;
|
||||
display: inline-block;
|
||||
z-index: 9999;
|
||||
padding: 0.2rem 0.7em;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
top: 0rem;
|
||||
transition: top 300ms;
|
||||
line-height: 2em;
|
||||
max-width:100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
nav#topbar a {
|
||||
display: inline-block;
|
||||
padding: 0 0.6em;
|
||||
line-height: 2em;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
nav#topbar .navbar-propic {
|
||||
margin-right: 0.3rem;
|
||||
border-radius: 0.2rem;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
nav#topbar a.align-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
nav img {
|
||||
height: 1.2em;
|
||||
display: inline;
|
||||
vertical-align:middle;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
nav a#mobileMenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav .navbar-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 924px) {
|
||||
|
||||
nav#topbar::before {
|
||||
transition-delay: 200ms;
|
||||
transition-property: background-image;
|
||||
}
|
||||
|
||||
nav#topbar:has(.navbar-container:not(.open))::before{
|
||||
background-image: url('/res/furizon.png');
|
||||
background-size: contain;
|
||||
background-position: 50%;
|
||||
background-origin: content-box;
|
||||
}
|
||||
|
||||
nav#topbar.closed {
|
||||
top: -100rem;
|
||||
}
|
||||
|
||||
nav#topbar a#mobileMenu {
|
||||
display: block;
|
||||
max-width: 3rem;
|
||||
}
|
||||
|
||||
nav#topbar .navbar-container {
|
||||
transition: max-height 200ms;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
nav#topbar .navbar-container:not(.open) {
|
||||
max-height: 0rem;
|
||||
}
|
||||
|
||||
nav#topbar .navbar-container.open {
|
||||
max-height: 30rem;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
svg.propic-border-filter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.propic-container {
|
||||
max-width:7em;
|
||||
margin:0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.propic-container img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.propic-container .absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.propic-filler {
|
||||
width: 100%;
|
||||
border-radius: 0.4em;
|
||||
margin: 0 auto;
|
||||
border: 4px solid #546e7a;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.propic {
|
||||
width:calc(100% - 4mm);
|
||||
border-radius:0.4em;
|
||||
margin:2mm;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.propic-border {
|
||||
top: 0%;
|
||||
z-index: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
border-radius:0.6em;
|
||||
border: none !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.propic-border-animation {
|
||||
min-width: 200%;
|
||||
min-height: 200%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
.propic-border.blurred {
|
||||
filter: blur(20px);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.propic-border.propic-animated-super .propic-border-animation {
|
||||
background: linear-gradient(90deg, #FA5E00 20%, #FAAA00 40%, #FB8C00 60%, #FA3200 80%, #FAC800 100%);
|
||||
animation: border-animation 2.5s linear 0ms infinite;
|
||||
}
|
||||
|
||||
.propic-border.propic-animated-normal .propic-border-animation {
|
||||
background: linear-gradient(90deg, #6124AB 20%, #AB248F 40%, #8E24AA 60%, #3524AB 80%, #AB243E 100%);
|
||||
animation: border-animation 2.5s linear 0ms infinite;
|
||||
}
|
||||
|
||||
@keyframes border-animation {
|
||||
0% {
|
||||
rotate: 0deg;
|
||||
}
|
||||
|
||||
100% {
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
|
||||
.propic-flag {
|
||||
z-index: 4;
|
||||
max-width: 2em;
|
||||
border-radius:2px;
|
||||
right: calc(0% - 0.3em);
|
||||
bottom: -0.1em;
|
||||
}
|
||||
|
||||
.propic-super {
|
||||
border:4px solid #fb8c00;
|
||||
}
|
||||
|
||||
.propic-normal {
|
||||
border:4px solid #8e24aa;
|
||||
}
|
||||
|
||||
.propic-base {
|
||||
border:4px solid #546e7a;
|
||||
}
|
||||
|
||||
.control-login-as:hover::before{
|
||||
background-color: #2f4e5cc5;
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content: 'Enter as';
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
z-index: 3;
|
||||
border-radius: 0.6rem;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
span.nsc-room-counter {
|
||||
font-size: medium;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
#intentFormAction #intentText {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
#room a[role=button] {
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
div.room-actions a>span {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
div.grid.people div.edit-disabled {
|
||||
pointer-events: none;
|
||||
filter: grayscale(1);
|
||||
user-select: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
div.grid.people div.edit-drag>div.propic-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
div.drag-over {
|
||||
border-color: #000;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
div.drag-forbidden {
|
||||
border-color: #c92121aa;
|
||||
}
|
||||
|
||||
div.interactless > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
div.room.complete {
|
||||
border-color: #21c929aa;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
div.room {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid transparent;
|
||||
margin-bottom: var(--spacing);
|
||||
}
|
||||
|
||||
div.room > h4 {
|
||||
user-select: none;
|
||||
margin-left: 1.6rem;
|
||||
}
|
||||
|
||||
div.room:nth-child(2n) {
|
||||
background-color: #ffffff55;
|
||||
}
|
||||
|
||||
div.room:nth-child(2n) {
|
||||
background-color: #cccccc55;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #2e9147aa;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #912e2eaa;
|
||||
}
|
||||
|
||||
/* Dark theme */
|
||||
@media only screen and (prefers-color-scheme: dark) {
|
||||
div.drag-over {
|
||||
border-color: #fff;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
div.drag-forbidden {
|
||||
border-color: #c92121aa;
|
||||
}
|
||||
|
||||
div.room {
|
||||
background-color: #16161655;
|
||||
}
|
||||
|
||||
div.room:nth-child(2n) {
|
||||
background-color: #20202055;
|
||||
}
|
||||
|
||||
}
|
131
room.py
|
@ -3,6 +3,9 @@ from sanic import Blueprint, exceptions
|
|||
from random import choice
|
||||
from ext import *
|
||||
from config import headers
|
||||
import os
|
||||
from image_util import generate_room_preview, get_room
|
||||
from utils import confirm_room_by_order
|
||||
|
||||
bp = Blueprint("room", url_prefix="/manage/room")
|
||||
|
||||
|
@ -18,6 +21,9 @@ async def room_create_post(request, order: Order):
|
|||
|
||||
if order.room_id:
|
||||
error = "You are already in another room. You need to delete (if it's yours) or leave it before creating another."
|
||||
|
||||
if order.daily:
|
||||
raise exceptions.BadRequest("You cannot create a room if you have a daily ticket!")
|
||||
|
||||
if not error:
|
||||
await order.edit_answer('room_name', name)
|
||||
|
@ -35,6 +41,9 @@ async def room_create(request, order: Order):
|
|||
|
||||
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
||||
if order.daily:
|
||||
raise exceptions.BadRequest("You cannot create a room if you have a daily ticket!")
|
||||
|
||||
tpl = request.app.ctx.tpl.get_template('create_room.html')
|
||||
return html(tpl.render(order=order))
|
||||
|
||||
|
@ -43,7 +52,7 @@ async def delete_room(request, order: Order):
|
|||
if not order:
|
||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
||||
if order.room_id != order.code:
|
||||
if not order.room_owner:
|
||||
raise exceptions.BadRequest("You are not allowed to delete room of others.")
|
||||
|
||||
if order.ans('room_confirmed'):
|
||||
|
@ -57,6 +66,7 @@ async def delete_room(request, order: Order):
|
|||
await order.edit_answer('room_members', None)
|
||||
await order.edit_answer('room_secret', None)
|
||||
await order.send_answers()
|
||||
remove_room_preview (order.code)
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
@bp.post("/join")
|
||||
|
@ -70,6 +80,9 @@ async def join_room(request, order: Order):
|
|||
|
||||
if order.room_id:
|
||||
raise exceptions.BadRequest("You are in another room already. Why would you join another?")
|
||||
|
||||
if order.daily:
|
||||
raise exceptions.BadRequest("You cannot join a room if you have a daily ticket!")
|
||||
|
||||
code = request.form.get('code').strip()
|
||||
room_secret = request.form.get('room_secret').strip()
|
||||
|
@ -87,6 +100,9 @@ async def join_room(request, order: Order):
|
|||
|
||||
if room_owner.room_confirmed:
|
||||
raise exceptions.BadRequest("The room you're trying to join has been confirmed already")
|
||||
|
||||
if room_owner.bed_in_room != order.bed_in_room:
|
||||
raise exceptions.BadRequest("This room's ticket is of a different type than yours!")
|
||||
|
||||
#if room_owner.pending_roommates and (order.code in room_owner.pending_roommates):
|
||||
#raise exceptions.BadRequest("What? You should never reach this check, but whatever...")
|
||||
|
@ -100,7 +116,7 @@ async def join_room(request, order: Order):
|
|||
|
||||
await room_owner.edit_answer('pending_roommates', ','.join(pending_roommates))
|
||||
await room_owner.send_answers()
|
||||
|
||||
remove_room_preview (code)
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
@bp.route("/kick/<code>")
|
||||
|
@ -123,7 +139,7 @@ async def kick_member(request, code, order: Order):
|
|||
|
||||
await order.send_answers()
|
||||
await to_kick.send_answers()
|
||||
|
||||
remove_room_preview (order.code)
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
@bp.route("/renew_secret")
|
||||
|
@ -164,6 +180,9 @@ async def cancel_request(request, order: Order):
|
|||
async def approve_roomreq(request, code, order: Order):
|
||||
if not order:
|
||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
||||
if not order.room_owner:
|
||||
raise exceptions.BadRequest("You are not the owner of the room!")
|
||||
|
||||
if not code in order.pending_roommates:
|
||||
raise exceptions.BadRequest("You cannot accept people that didn't request to join your room")
|
||||
|
@ -187,7 +206,7 @@ async def approve_roomreq(request, code, order: Order):
|
|||
|
||||
await pending_member.send_answers()
|
||||
await order.send_answers()
|
||||
|
||||
remove_room_preview(order.code)
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
@bp.route("/leave")
|
||||
|
@ -211,13 +230,16 @@ async def leave_room(request, order: Order):
|
|||
|
||||
await room_owner.send_answers()
|
||||
await order.send_answers()
|
||||
|
||||
remove_room_preview (order.room_id)
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
@bp.route("/reject/<code>")
|
||||
async def reject_roomreq(request, code, order: Order):
|
||||
if not order:
|
||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
||||
if not order.room_owner:
|
||||
raise exceptions.BadRequest("You are not the owner of the room!")
|
||||
|
||||
if not code in order.pending_roommates:
|
||||
raise exceptions.BadRequest("You cannot reject people that didn't request to join your room")
|
||||
|
@ -240,6 +262,32 @@ async def reject_roomreq(request, code, order: Order):
|
|||
await order.send_answers()
|
||||
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
@bp.post("/rename")
|
||||
async def rename_room(request, order: Order):
|
||||
if not order:
|
||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||
|
||||
if not order.room_owner:
|
||||
raise exceptions.BadRequest("You are not the owner of the room!")
|
||||
|
||||
if not order.room_id:
|
||||
raise exceptions.BadRequest("Try joining a room before renaming it.")
|
||||
|
||||
if order.room_confirmed:
|
||||
raise exceptions.BadRequest("You can't rename a confirmed room!")
|
||||
|
||||
if order.room_id != order.code:
|
||||
raise exceptions.BadRequest("You are not allowed to rename rooms of others.")
|
||||
|
||||
name = request.form.get('name')
|
||||
if len(name) > 64 or len(name) < 4:
|
||||
raise exceptions.BadRequest("Your room name is invalid. Please try another one.")
|
||||
|
||||
await order.edit_answer("room_name", name)
|
||||
await order.send_answers()
|
||||
remove_room_preview(order.code)
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
@bp.route("/confirm")
|
||||
async def confirm_room(request, order: Order, quotas: Quotas):
|
||||
|
@ -252,54 +300,31 @@ async def confirm_room(request, order: Order, quotas: Quotas):
|
|||
if order.room_id != order.code:
|
||||
raise exceptions.BadRequest("You are not allowed to confirm rooms of others.")
|
||||
|
||||
if quotas.get_left(len(order.room_members)) == 0:
|
||||
raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
|
||||
# This is not needed anymore you buy tickets already
|
||||
#if quotas.get_left(len(order.room_members)) == 0:
|
||||
# raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
|
||||
|
||||
room_members = []
|
||||
for m in order.room_members:
|
||||
if m == order.code:
|
||||
res = order
|
||||
else:
|
||||
res = await request.app.ctx.om.get_order(code=m)
|
||||
|
||||
if res.room_id != order.code:
|
||||
raise exceptions.BadRequest("Please contact support: some of the members in your room are actually somewhere else")
|
||||
|
||||
if res.status != 'paid':
|
||||
raise exceptions.BadRequest("Somebody hasn't paid.")
|
||||
|
||||
room_members.append(res)
|
||||
|
||||
for rm in room_members:
|
||||
await rm.edit_answer('room_id', order.code)
|
||||
await rm.edit_answer('room_confirmed', "True")
|
||||
await rm.edit_answer('pending_roommates', None)
|
||||
await rm.edit_answer('pending_room', None)
|
||||
|
||||
thing = {
|
||||
'order': order.code,
|
||||
'addon_to': order.position_positionid,
|
||||
'item': ITEM_IDS['room'],
|
||||
'variation': ROOM_MAP[len(room_members)]
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing)
|
||||
|
||||
if res.status_code != 201:
|
||||
raise exceptions.BadRequest("Something has gone wrong! Please contact support immediately")
|
||||
|
||||
'''for rm in room_members:
|
||||
if rm.code == order.code: continue
|
||||
thing = {
|
||||
'order': rm.code,
|
||||
'addon_to': rm.position_positionid,
|
||||
'item': ITEM_IDS['room'],
|
||||
'variation': ROOM_MAP[len(room_members)]
|
||||
}
|
||||
res = await client.post(join(base_url, "orderpositions/"), headers=headers, json=thing) '''
|
||||
|
||||
for rm in room_members:
|
||||
await rm.send_answers()
|
||||
confirm_room_by_order(order, request)
|
||||
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
async def get_room_with_order (request, code):
|
||||
order_data = await request.app.ctx.om.get_order(code=code)
|
||||
if not order_data or not order_data.room_owner: return None
|
||||
|
||||
def remove_room_preview(code):
|
||||
preview_file = f"res/rooms/{code}.jpg"
|
||||
try:
|
||||
if os.path.exists(preview_file): os.remove(preview_file)
|
||||
except Exception as ex:
|
||||
if (EXTRA_PRINTS): logger.exception(str(ex))
|
||||
|
||||
@bp.route("/view/<code>")
|
||||
async def get_view(request, code):
|
||||
room_file_name = f"res/rooms/{code}.jpg"
|
||||
room_data = await get_room(request, code)
|
||||
if not room_data: raise exceptions.NotFound("No room was found with that code.")
|
||||
if not os.path.exists(room_file_name):
|
||||
await generate_room_preview(request, code, room_data)
|
||||
tpl = request.app.ctx.tpl.get_template('view_room.html')
|
||||
return html(tpl.render(preview=room_file_name, room_data=room_data))
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
|
||||
python3 app.py 2>&1 | tee -a log.txt
|
39
stats.py
|
@ -1,12 +1,41 @@
|
|||
from sanic.response import html
|
||||
from sanic import Blueprint
|
||||
from sanic import Blueprint, Request
|
||||
from messages import NOSECOUNT
|
||||
from ext import *
|
||||
|
||||
bp = Blueprint("stats", url_prefix="/manage")
|
||||
|
||||
@bp.route("/nosecount")
|
||||
async def nose_count(request, order: Order):
|
||||
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e']}
|
||||
@bp.route("/sponsorcount")
|
||||
async def sponsor_count(request, order: Order):
|
||||
await request.app.ctx.om.update_cache()
|
||||
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: x[1].ans('fursona_name')) if value.status not in ['c', 'e']}
|
||||
|
||||
tpl = request.app.ctx.tpl.get_template('nosecount.html')
|
||||
tpl = request.app.ctx.tpl.get_template('sponsorcount.html')
|
||||
return html(tpl.render(orders=orders, order=order))
|
||||
|
||||
def calc_filter(orders: dict, filter_cmd: str, order: Order) -> tuple[dict, str]:
|
||||
if not filter_cmd or len(filter_cmd) == 0 or not orders or len(orders.keys()) == 0: return
|
||||
if filter_cmd.lower() == "capacity":
|
||||
return {key:value for key,value in orders.items() if (not value.room_confirmed and value.bed_in_room == order.bed_in_room)}, NOSECOUNT['filters'][filter_cmd.lower()]
|
||||
else:
|
||||
return None, None
|
||||
|
||||
@bp.route("/nosecount")
|
||||
async def nose_count(request: Request, order: Order):
|
||||
await request.app.ctx.om.update_cache()
|
||||
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e']}
|
||||
filtered: dict = None
|
||||
filter_message: str = None
|
||||
for query in request.query_args:
|
||||
if query[0] == "filter" and order:
|
||||
filtered, filter_message = calc_filter(orders, query[1], order) if filter else None
|
||||
tpl = request.app.ctx.tpl.get_template('nosecount.html')
|
||||
return html(tpl.render(orders=orders, order=order, filtered=filtered, filter_header=filter_message))
|
||||
|
||||
@bp.route("/fursuitcount")
|
||||
async def fursuit_count(request, order: Order):
|
||||
await request.app.ctx.om.update_cache()
|
||||
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: x[1].ans('fursona_name')) if value.status not in ['c', 'e']}
|
||||
|
||||
tpl = request.app.ctx.tpl.get_template('fursuitcount.html')
|
||||
return html(tpl.render(orders=orders, order=order))
|
|
@ -0,0 +1,78 @@
|
|||
from config import *
|
||||
import requests
|
||||
import datetime
|
||||
import time
|
||||
|
||||
ROOM_CAPACITY_MAP = {
|
||||
0: 0,
|
||||
# SACRO CUORE
|
||||
83: 11,
|
||||
67: 50,
|
||||
68: 45,
|
||||
69: 84,
|
||||
70: 10,
|
||||
|
||||
# OVERFLOW 1
|
||||
75: 50
|
||||
}
|
||||
|
||||
def ans(data, name):
|
||||
for p in data['positions']:
|
||||
for a in p['answers']:
|
||||
if a.get('question_identifier', None) == name:
|
||||
if a['answer'] in ['True', 'False']:
|
||||
return bool(a['answer'] == 'True')
|
||||
return a['answer']
|
||||
return None
|
||||
|
||||
def getOrders():
|
||||
ret = []
|
||||
p = 0
|
||||
|
||||
while 1:
|
||||
p += 1
|
||||
res = requests.get(f"{base_url_event}orders/?page={p}", headers=headers)
|
||||
|
||||
if res.status_code == 404: break
|
||||
|
||||
data = res.json()
|
||||
for o in data['results']:
|
||||
|
||||
roomType = 0
|
||||
|
||||
for pos in o['positions']:
|
||||
if pos['item'] == ITEMS_ID_MAP['bed_in_room']:
|
||||
roomType = pos['variation']
|
||||
|
||||
ret.append({"code": o['code'], "fname": ans(o, 'fursona_name'), "rType": roomType, "date": o['datetime']})
|
||||
return ret
|
||||
|
||||
ordersCode = set()
|
||||
ordersTime = set()
|
||||
ordersFName = set()
|
||||
while True:
|
||||
#try:
|
||||
newOrders = getOrders()
|
||||
shouldSleep = True
|
||||
for o in newOrders:
|
||||
if o['code'] not in ordersCode and not o['date'] in ordersTime and not o['fname'] in ordersFName:
|
||||
|
||||
remainingInRoomType = ROOM_CAPACITY_MAP[o['rType']]
|
||||
remainingInRoomType -= 1
|
||||
ROOM_CAPACITY_MAP[o['rType']] = remainingInRoomType
|
||||
|
||||
roomCapacitiesStr = ", ".join(str(x).rjust(2, "0") for x in ROOM_CAPACITY_MAP.values())
|
||||
#dateStr = datetime.datetime.now().isoformat()
|
||||
|
||||
print(f"[{o['date']}] {len(ordersCode)} - [{o['code']}] New order! FursonaName: {o['fname'].ljust(24)} - Room capacities: {roomCapacitiesStr}")
|
||||
|
||||
shouldSleep = False
|
||||
time.sleep(0.05)
|
||||
ordersCode.add(o['code'])
|
||||
ordersTime.add(o['date'])
|
||||
ordersFName.add(o['fname'])
|
||||
#except:
|
||||
# print("Exception occurred!")
|
||||
# pass
|
||||
if shouldSleep:
|
||||
time.sleep(1)
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
/usr/bin/tar -czvf /tmp/backupRclone.tar.gz /home/ /root/ /etc/ /var/backups/ /var/log/ /var/mail/ /var/pretix-data/ /var/prometheus-data/ /var/spool/ /var/www/ /var/lib/grafana/ /var/lib/redis/
|
||||
|
||||
/usr/bin/rclone sync /tmp/backupRclone.tar.gz webservice:/backups/backupRclone.tar.gz -P --stats=1s --bwlimit 15M
|
||||
|
||||
/usr/bin/rm /tmp/backupRclone.tar.gz
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
source /root/.profile
|
||||
|
||||
/usr/bin/tar -czf /tmp/backupRclone.tar.gz /home/ /root/ /etc/ /var/backups/ /var/log/ /var/mail/ /var/pretix-data/ /var/prometheus-data/ /var/spool/ /var/www/ /var/lib/grafana/ /var/lib/redis/
|
||||
|
||||
/usr/bin/rclone sync /tmp/backupRclone.tar.gz webservice:/backups/backupRclone.tar.gz --bwlimit 15M
|
||||
|
||||
/usr/bin/rm /tmp/backupRclone.tar.gz
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
from os import listdir, remove
|
||||
from os.path import isfile, join
|
||||
import datetime
|
||||
import subprocess
|
||||
|
||||
PRETIX_BACKUP = False
|
||||
WEBINT_BACKUP = False
|
||||
|
||||
BACKUP_DIR_PRETIX = "/home/pretix/backups/"
|
||||
BACKUP_DIR_WEBINT = "/home/webint/backups/"
|
||||
|
||||
MAX_FILE_NO = 14
|
||||
|
||||
COMMAND_PRETIX_POSTGRES = "pg_dump -F p pretix | gzip > %s" # Restore with psql -f %s
|
||||
COMMAND_PRETIX_DATA = "tar -cf %s /var/pretix-data" # Restore with tar -xvf %s. To make .secret readable I used setfacl -m u:pretix:r /var/pretix-data/.secret
|
||||
COMMAND_WEBINT = "tar -cf %s /home/webint/furizon_webint" # Restore with tar -xvf %s
|
||||
|
||||
|
||||
def deleteOlder(path : str, prefix : str, postfix : str):
|
||||
backupFileNames = sorted([f for f in listdir(path) if (isfile(join(path, f)) and f.startswith(prefix) and f.endswith(postfix))])
|
||||
while(len(backupFileNames) > MAX_FILE_NO):
|
||||
print(f"Removing {backupFileNames[0]}")
|
||||
remove(join(path, backupFileNames[0]))
|
||||
backupFileNames.pop(0)
|
||||
|
||||
def genFileName(prefix : str, postfix : str):
|
||||
return prefix + "_" + datetime.datetime.now(datetime.UTC).strftime('%Y%m%d-%H%M%S') + "_" + postfix
|
||||
|
||||
def runBackup(prefix : str, postfix : str, path : str, command : str):
|
||||
deleteOlder(path, prefix, postfix)
|
||||
name = join(path, genFileName(prefix, postfix))
|
||||
process = subprocess.Popen(command % name, shell=True)
|
||||
process.wait()
|
||||
|
||||
|
||||
|
||||
if(PRETIX_BACKUP):
|
||||
runBackup("pretix_postres", "backup.sql.gz", join(BACKUP_DIR_PRETIX, "postgres"), COMMAND_PRETIX_POSTGRES)
|
||||
runBackup("pretix_data", "backup.tar.gz", join(BACKUP_DIR_PRETIX, "data"), COMMAND_PRETIX_DATA)
|
||||
|
||||
if(WEBINT_BACKUP):
|
||||
runBackup("webint_full", "backup.tar.gz", BACKUP_DIR_WEBINT, COMMAND_WEBINT)
|
|
@ -0,0 +1,91 @@
|
|||
import psutil
|
||||
import time
|
||||
import sys
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import response as res
|
||||
|
||||
DEV_MODE = False
|
||||
ACCESS_LOG = False
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
@app.route("/")
|
||||
async def main(req):
|
||||
return res.text("I\'m a teapot", status=418)
|
||||
|
||||
@app.route("/metrics")
|
||||
async def metrics(req):
|
||||
out = []
|
||||
|
||||
cpus = psutil.cpu_percent(percpu=True)
|
||||
totalCpu = 0
|
||||
for i in range(len(cpus)):
|
||||
out.append(f'monitor_cpu_core_usage_percent{{core="{i}"}} {cpus[i]}')
|
||||
totalCpu += cpus[i]
|
||||
out.append(f'monitor_cpu_core_usage_percent{{core="avg"}} {"%.2f" % (totalCpu / len(cpus))}')
|
||||
|
||||
diskIo = psutil.disk_io_counters(nowrap=True)
|
||||
out.append(f'monitor_diskio{{value="read_count"}} {diskIo.read_count}')
|
||||
out.append(f'monitor_diskio{{value="write_count"}} {diskIo.write_count}')
|
||||
out.append(f'monitor_diskio{{value="read_bytes"}} {diskIo.read_bytes}')
|
||||
out.append(f'monitor_diskio{{value="write_bytes"}} {diskIo.write_bytes}')
|
||||
out.append(f'monitor_diskio{{value="read_time"}} {diskIo.read_time}')
|
||||
out.append(f'monitor_diskio{{value="write_time"}} {diskIo.write_time}')
|
||||
|
||||
disks = psutil.disk_partitions()
|
||||
for disk in disks:
|
||||
try:
|
||||
dUsage = psutil.disk_usage(disk.mountpoint)
|
||||
out.append(f'monitor_disk_usage_percent{{partition="{disk.mountpoint}"}} {dUsage.percent}')
|
||||
except:
|
||||
pass
|
||||
|
||||
mem = psutil.virtual_memory()
|
||||
out.append(f'monitor_memory{{value="total"}} {mem.total}')
|
||||
out.append(f'monitor_memory{{value="available"}} {mem.available}')
|
||||
out.append(f'monitor_memory{{value="percent"}} {mem.percent}')
|
||||
out.append(f'monitor_memory{{value="used"}} {mem.used}')
|
||||
out.append(f'monitor_memory{{value="free"}} {mem.free}')
|
||||
|
||||
swap = psutil.swap_memory()
|
||||
out.append(f'monitor_swap{{value="total"}} {swap.total}')
|
||||
out.append(f'monitor_swap{{value="used"}} {swap.used}')
|
||||
out.append(f'monitor_swap{{value="free"}} {swap.free}')
|
||||
out.append(f'monitor_swap{{value="percent"}} {swap.percent}')
|
||||
out.append(f'monitor_swap{{value="sin"}} {swap.sin}')
|
||||
out.append(f'monitor_swap{{value="sout"}} {swap.sout}')
|
||||
|
||||
bootTime = psutil.boot_time()
|
||||
out.append(f'monitor_boot_time{{}} {int(time.time() - bootTime)}')
|
||||
|
||||
netioConnections = psutil.net_connections()
|
||||
out.append(f'monitor_netio_connections{{}} {len(netioConnections)}')
|
||||
netioCounters = psutil.net_io_counters(nowrap=True)
|
||||
out.append(f'monitor_netio_counters{{value="bytes_sent"}} {netioCounters.bytes_sent}')
|
||||
out.append(f'monitor_netio_counters{{value="bytes_recv"}} {netioCounters.bytes_recv}')
|
||||
out.append(f'monitor_netio_counters{{value="packets_sent"}} {netioCounters.packets_sent}')
|
||||
out.append(f'monitor_netio_counters{{value="packets_recv"}} {netioCounters.packets_recv}')
|
||||
out.append(f'monitor_netio_counters{{value="errin"}} {netioCounters.errin}')
|
||||
out.append(f'monitor_netio_counters{{value="errout"}} {netioCounters.errout}')
|
||||
out.append(f'monitor_netio_counters{{value="dropin"}} {netioCounters.dropin}')
|
||||
out.append(f'monitor_netio_counters{{value="dropout"}} {netioCounters.dropout}')
|
||||
|
||||
return res.text("\n".join(out), status=200)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ip = "127.0.0.1"
|
||||
port = 2611
|
||||
if(len(sys.argv) > 1):
|
||||
ip = sys.argv[1]
|
||||
if(len(sys.argv) > 2):
|
||||
try:
|
||||
port = int(sys.argv[2])
|
||||
except:
|
||||
print("Port must be a numeric value!")
|
||||
exit(1)
|
||||
if(port < 1 or port > 0xffff):
|
||||
print("Port must be in [1, 65535] range!")
|
||||
exit(1)
|
||||
app.run(host=ip, port=port, dev=DEV_MODE, access_log=ACCESS_LOG)
|
|
@ -0,0 +1,32 @@
|
|||
import smtplib
|
||||
import ssl
|
||||
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
from secrets import *
|
||||
|
||||
SMTP_HOST = 'smtp.office365.com'
|
||||
SMTP_USER = 'webservice@furizon.net'
|
||||
SMTP_PORT = 587
|
||||
|
||||
plain_body = "Test aaaa"
|
||||
|
||||
plain_text = MIMEText(plain_body, "plain")
|
||||
message = MIMEMultipart("alternative")
|
||||
message.attach(plain_text)
|
||||
message['Subject'] = '[Furizon] This is a test!'
|
||||
message['From'] = 'Furizon <webservice@furizon.net>'
|
||||
message['To'] = f"Luca Sorace <strdjn@gmail.com>"
|
||||
|
||||
print("Start")
|
||||
context = ssl.create_default_context()
|
||||
print("Context created")
|
||||
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as sender:
|
||||
print("Created sender obj")
|
||||
sender.starttls(context=context)
|
||||
print("Tls started")
|
||||
sender.login(SMTP_USER, SMTP_PASSWORD)
|
||||
print("Logged in")
|
||||
print(sender.sendmail(message['From'], message['to'], message.as_string()))
|
||||
print("Mail sent")
|
|
@ -0,0 +1,31 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Admin panel{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<script src="/res/scripts/adminManager.js"></script>
|
||||
<header>
|
||||
<picture>
|
||||
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
|
||||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
<!-- Quick controls -->
|
||||
<h2>Admin panel</h2>
|
||||
<p>Data</p>
|
||||
<a href="/manage/admin/cache/clear" role="button" title="Reload the orders' data and the re-sync items' indexes from pretix">Clear cache</a>
|
||||
<a download href="/manage/admin/export/export" role="button" title="Will export most of informations about the orders">Export CSV</a>
|
||||
<a download href="/manage/admin/export/hotel" role="button" title="Will export a CSV for the hotel accomodation">Export hotel CSV</a>
|
||||
<hr>
|
||||
<p>Rooms</p>
|
||||
<a href="/manage/nosecount" role="button" title="Shortcut to the nosecount's admin data">Manage rooms</a>
|
||||
<a href="/manage/admin/room/verify" role="button" title="Will unconfirm rooms that fail the default check. Useful when editing answers from Pretix">Verify Rooms</a>
|
||||
<a href="/manage/admin/room/wizard" role="button" title="Auto fill unconfirmed rooms. You can review and edit matches before confirming.">Fill Rooms</a>
|
||||
<hr>
|
||||
<p>Profiles</p>
|
||||
<a href="#" onclick="confirmAction('propicReminder', this)" role="button" title="Will email all people who haven't uploaded a badge, yet" action="/manage/admin/propic/remind">Remind badge upload</a>
|
||||
<a href="/manage/admin/room/autoconfirm" role="button" title="Will confirm all the full rooms that are still unconfirmed">Auto-confirm Rooms</a>
|
||||
<hr>
|
||||
{% include 'components/confirm_action_modal.html' %}
|
||||
</main>
|
||||
|
||||
{% endblock %}
|
|
@ -1,21 +1,15 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{% block head %}{% endblock %}
|
||||
<meta charset="utf-8" />
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=400rem" />
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<link rel="stylesheet" href="/res/pico.min.css">
|
||||
<link rel="stylesheet" href="/res/styles/base.css">
|
||||
<link rel="icon" type="image/x-icon" href="/res/icons/favicon.ico">
|
||||
<style>
|
||||
|
||||
.propic-container {max-width:6em;margin:0 auto;}
|
||||
.propic-container img {display:block;}
|
||||
.propic {width:100%;border-radius:0.4em;margin:0 auto;border:4px solid #546e7a;}
|
||||
|
||||
.propic-flag {max-width:2em;margin-top:-1em;margin-left:auto;transform:translateX(0.7em);margin-right:0em;border-radius:2px;}
|
||||
.propic-super {border:4px solid #fb8c00;}
|
||||
.propic-normal {border:4px solid #8e24aa;}
|
||||
|
||||
.people div {text-align:center;}
|
||||
.people h3, .people p, .people h5 {margin:0;}
|
||||
.people {grid-auto-flow:row;}
|
||||
|
@ -23,14 +17,12 @@
|
|||
|
||||
main {min-height: 30em;}
|
||||
|
||||
mark {background:#0f0;}
|
||||
h1 img {max-height:1.4em;}
|
||||
.notice {padding:0.8em;border-radius:3px;background:#e53935;color:#eee;}
|
||||
.notice a {color:#eee;text-decoration:underline;}
|
||||
.container {max-width:40em;padding:1em;box-sizing:border-box;}
|
||||
.container {max-width:50em;padding:1em;box-sizing:border-box;}
|
||||
td > a[role=button] {padding: 0.3em 0.7em;}
|
||||
td {padding-left: 0.2em;padding-right: 0.2em;}
|
||||
a[role=button] {margin: 0.25em 0;}
|
||||
td > input[type=file] {margin:0;padding:0;}
|
||||
section {margin-bottom:3em;}
|
||||
@media (min-width: 500px) {body .grid {grid-template-columns: repeat(auto-fit, minmax(0%, 1fr));}}
|
||||
|
@ -39,15 +31,8 @@
|
|||
|
||||
summary[role=button] {background:var(--primary-focus);color:var(--contrast);}
|
||||
summary img, td img {height:1.2em;width:2em;box-sizing: border-box;}
|
||||
|
||||
@media only screen and (prefers-color-scheme: dark) {
|
||||
.icon {filter: invert(1);}
|
||||
}
|
||||
|
||||
nav {justify-content: normal;padding:0 0.5em;background:var(--card-background-color);}
|
||||
nav a {display:inline-block;padding:0 0.6em;line-height:2em;margin:0;white-space:nowrap}
|
||||
nav img {height:1.2em;display:inline;vertical-align:middle;box-sizing: border-box;}
|
||||
nav {display:block;}
|
||||
|
||||
|
||||
body .grid.people { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)) !important }
|
||||
.status {float:right;}
|
||||
|
@ -57,41 +42,16 @@
|
|||
footer img {height:1.3em;}
|
||||
|
||||
.tag {background:var(--primary);color:var(--contrast);font-size:0.8em;font-weight:600;padding:0.1em 0.3em;border-radius:3px;}
|
||||
|
||||
.grid_2x2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Matomo -->
|
||||
<script>
|
||||
var _paq = window._paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(["setCampaignNameKey", "private_area"]);
|
||||
{% if order %}
|
||||
_paq.push(['setUserId', '{{order.code}}']);
|
||||
{% endif %}
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function() {
|
||||
var u="https://y.foxo.me/";
|
||||
_paq.push(['setTrackerUrl', u+'matomo.php']);
|
||||
_paq.push(['setSiteId', '2']);
|
||||
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||
})();
|
||||
</script>
|
||||
<noscript><p><img src="https://y.foxo.me/matomo.php?idsite=2&rec=1" style="border:0;" alt="" /></p></noscript>
|
||||
<!-- End Matomo Code -->
|
||||
<script src="/res/scripts/base.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav id="topbar">
|
||||
{% if order %}
|
||||
<a href="/manage/welcome">Your Booking</a>
|
||||
{% endif %}
|
||||
<a href="/manage/nosecount">Nose Count</a>
|
||||
{% if order %}
|
||||
<a href="/manage/carpooling">Carpooling</a>
|
||||
<a style="float:right;" href="/manage/logout">Logout</a>
|
||||
{% endif %}
|
||||
<br clear="both" />
|
||||
</nav>
|
||||
<body onscroll="onScrollNav()">
|
||||
{% include 'blocks/navbar.html' %}
|
||||
{% block main %}{% endblock %}
|
||||
|
||||
<script type="text/javascript">
|
||||
|
@ -106,7 +66,7 @@
|
|||
});
|
||||
</script>
|
||||
<footer>
|
||||
Made in 🇮🇹 by <img src="/res/icons/silhouette.svg" class="icon" /> · <a href="/manage/privacy">Privacy</a>
|
||||
Made in 🇮🇹 by <a href="https://lab.foxo.me"><img src="/res/icons/silhouette.svg" class="icon" /></a> and maintained by <a href="https://twitter.com/stranckV2">Stranck</a> and <a href="https://about.woffo.ovh/">DrewThaWoof</a><br><a href="/manage/privacy">Privacy</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,35 +1,33 @@
|
|||
<details id="badge">
|
||||
<summary role="button"><img src="/res/icons/badge.svg" class="icon"/>Badge Customization {% if not order.ans('propic') %}<span class="status">⚠️</span>{% endif %}</summary>
|
||||
<summary role="button"><img src="/res/icons/badge.svg" class="icon"/>Badge Customization {% if not order.isBadgeValid() %}<span class="status">⚠️</span>{% endif %}</summary>
|
||||
{# Badge is always shown #}
|
||||
<h2>Badge</h2>
|
||||
{% if order.propic_locked %}
|
||||
<p class="notice">⚠️ You have been limited from further editing your profile pic.</p>
|
||||
{% endif %}
|
||||
{% if (not order.ans('propic')) or (order.ans('is_fursuiter') != 'No' and not order.ans('propic_fursuiter')) %}
|
||||
{% if not order.ans('propic') or (order.is_fursuiter and not order.ans('propic_fursuiter')) %}
|
||||
<p class="notice">⚠️ One or more badge pictures are missing! This will cause you badge to be empty, so make sure to upload something before the deadline!</p>
|
||||
{% endif %}
|
||||
<form method="POST" enctype="multipart/form-data" action="/manage/propic/upload">
|
||||
<div class="grid" style="text-align:center;margin-bottom:1em;">
|
||||
<div>
|
||||
{% with current=order, order=order, imgSrc='/res/propic/' + (order.ans('propic') or 'default.png'), effects = false %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<p>Normal Badge</p>
|
||||
{% if not order.ans('propic') %}
|
||||
<input type="file" value="" accept="image/jpeg,image/png" name="propic" />
|
||||
{% else %}
|
||||
<div class="propic-container">
|
||||
<img src="/res/propic/{{order.ans('propic') or 'default.png'}}" class="propic" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>Normal Badge</p>
|
||||
</div>
|
||||
{% if order.ans('is_fursuiter') != 'No' %}
|
||||
{% if order.is_fursuiter %}
|
||||
<div>
|
||||
{% with current=order, order=order, imgSrc='/res/propic/' + (order.ans('propic_fursuiter') or 'default.png'), effects = false %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<p>Fursuit Badge</p>
|
||||
{% if not order.ans('propic_fursuiter') %}
|
||||
<input type="file" value="" accept="image/jpeg,image/png" name="propic_fursuiter" />
|
||||
{% else %}
|
||||
<div class="propic-container">
|
||||
<img src="/res/propic/{{order.ans('propic_fursuiter') or 'default.png'}}" class="propic" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<p>Fursuit Badge</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -38,21 +36,16 @@
|
|||
<p class="notice">⚠️ The deadline to upload pictures for the badge has expired. For last-minute changes, please contact the support over at <a href="mailto:info@furizon.net">info@furizon.net</a>. If your badge has been printed already, changing it will incur in a 2€ fee. You can also get extra badges at the reception for the same price. If you upload a propic now, it might not be printed on time.</p>
|
||||
{% else %}
|
||||
<p><em>
|
||||
Min size: 64x64 - Max Size: 5MB, 2048x2048 - Formats: jpg, png<br />
|
||||
Min size: {{PROPIC_MIN_SIZE[0]}}x{{PROPIC_MIN_SIZE[1]}} - Max Size: {{PROPIC_MAX_FILE_SIZE}}, {{PROPIC_MAX_SIZE[0]}}x{{PROPIC_MAX_SIZE[1]}} - Formats: jpg, png<br />
|
||||
Photos whose aspect ratio is not a square will be cropped
|
||||
Badge photos must clearly show the fursona/fursuit head.<br />Memes and low quality images will be removed and may limit your ability to upload pics in the future.
|
||||
</em></p>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid">
|
||||
{% if order.ans('propic') %}
|
||||
<input type="submit" name="submit" value="Delete main image" {{'disabled' if time() > PROPIC_DEADLINE else ''}} />
|
||||
{% endif %}
|
||||
{% if order.ans('propic_fursuiter') %}
|
||||
<input type="submit" name="submit" value="Delete fursuit image" {{'disabled' if time() > PROPIC_DEADLINE else ''}} />
|
||||
{% endif %}
|
||||
{% if (not order.ans('propic')) or (order.ans('is_fursuiter') != 'No' and not order.ans('propic_fursuiter')) %}
|
||||
<input type="submit" name="submit" value="Upload" />
|
||||
{% endif %}
|
||||
<div class="grid grid_2x2">
|
||||
<input style="grid-area: 1 / 1 / 2 / 3;" type="submit" name="submit" value="Upload" {{'disabled' if (order.ans('propic') and order.ans('propic_fursuiter')) else ''}} />
|
||||
<input style="grid-area: 2 / 1 / 3 / 2;" type="submit" name="submit" value="Delete main image" {{'disabled' if (time() > PROPIC_DEADLINE or not order.ans('propic')) else ''}} />
|
||||
<input style="grid-area: 2 / 2 / 3 / 3;" type="submit" name="submit" value="Delete fursuit image" {{'disabled' if (time() > PROPIC_DEADLINE or not order.ans('propic_fursuiter')) else ''}} />
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<nav id="topbar">
|
||||
<a id="mobileMenu">
|
||||
<img class="nav-hamburger icon" src="/res/icons/menu.svg"/>
|
||||
</a>
|
||||
<div class="navbar-container">
|
||||
{% if order %}
|
||||
<a href="/manage/welcome">
|
||||
<img class="navbar-propic" src="{{'/res/propic/' + (order.ans('propic') or 'default.png')}}"></img>{{order.ans('fursona_name')}}'s Booking
|
||||
</a>
|
||||
{% if order.isAdmin() %}<a class="" href="/manage/admin">Admin panel</a>{% endif %}
|
||||
{% endif %}
|
||||
<a href="/manage/nosecount">Nose Count</a>
|
||||
<a href="/manage/fursuitcount">Fursuit Count</a>
|
||||
<a href="/manage/sponsorcount" class="rainbow-text">Sponsor Count</a>
|
||||
{% if order %}
|
||||
<a href="/manage/carpooling">Carpooling</a>
|
||||
|
||||
|
||||
<a class="align-right" href="/manage/logout">Logout</a>
|
||||
{% endif %}
|
||||
<br clear="both" />
|
||||
</div>
|
||||
</nav>
|
|
@ -18,7 +18,7 @@
|
|||
</tr>
|
||||
</table>
|
||||
{% if order.status == 'paid' and order.room_confirmed %}
|
||||
<p style="text-align:right;"><a href="/manage/download_ticket?name=BEYOND-{{order.code}}.pdf" role="button">Download ticket</a></p>
|
||||
<p style="text-align:right;"><a href="/manage/download_ticket?name=OVERLORD-{{order.code}}.pdf" role="button">Download ticket</a></p>
|
||||
{% endif %}
|
||||
{% if order.status != 'paid' %}
|
||||
<a href="{{order.url}}"><button>Payment area</button></a>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<div class="propic-container" >
|
||||
{% if effects and (not order.sponsorship == None) %}
|
||||
<div aria-hidden="true" class="absolute propic-border {{'propic-animated-' + (order.sponsorship or 'base')}}">
|
||||
<div aria-hidden="true" class="propic-border-animation"></div>
|
||||
</div>
|
||||
<div aria-hidden="true" class="absolute propic-border blurred {{'propic-animated-' + (order.sponsorship or 'base')}}">
|
||||
<div aria-hidden="true" class="propic-border-animation"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if current and current.isAdmin () and (not current.code == order.code ) and not nologin %}
|
||||
<a class="control-login-as" href="/manage/admin/loginas/{{order.code}}">
|
||||
{% endif %}
|
||||
<img alt="{{order.ans('fursona_name') if order.ans('fursona_name') else 'A user'}}'s profile picture" src="{{imgSrc}}" class="absolute propic {{(('propic-' + order.sponsorship) if not effects else '') if order.sponsorship else 'propic-base'}}"/>
|
||||
<svg aria-hidden="true" alt="" class="propic-filler {{(('propic-' + order.sponsorship) if not effects else '') if order.sponsorship else 'propic-base'}}" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"></svg>
|
||||
{% if current and current.isAdmin () %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if flag %}
|
||||
<img alt="flag" class="absolute propic-flag" src="/res/flags/{{order.country.lower()}}.svg"/>
|
||||
{% endif %}
|
||||
</div>
|
|
@ -1,12 +1,18 @@
|
|||
<details id="room">
|
||||
<summary role="button"><img src="/res/icons/bedroom.svg" class="icon"/> Accomodation & Roommates <span class="status">{% if not order.room_confirmed %}⚠️{% endif %}</span></summary>
|
||||
<h2>Your room {% if room_members %}- {{room_members[0].ans('room_name')}}{% endif %}</h2>
|
||||
<summary role="button"><img src="/res/icons/bedroom.svg" class="icon"/> Accomodation & Roommates {% if not order.room_confirmed %}<span class="status">⚠️</span>{% endif %}</summary>
|
||||
<h2 style="margin-bottom:0;">Your room {% if room_members %}- {{room_members[0].ans('room_name')}}{% endif %}</h2>
|
||||
<p><b>Room's type:</b> {{ROOM_TYPE_NAMES[order.bed_in_room]}}.</p>
|
||||
<p class="notice" style="background:#0881c0"><b>Note! </b> Only people with the same room type can be roommates. If you need help, contact the <a href="https://furizon.net/contact/">Furizon's Staff</a>.</p>
|
||||
|
||||
{% if not order.room_confirmed %}
|
||||
<p class="notice" style="background:#0881c0"><b><a href="/manage/nosecount?filter=capacity">Check here</a> for any fur who share your room type.</p>
|
||||
{% endif %}
|
||||
|
||||
{# Show alert if room owner has wrong people inside #}
|
||||
|
||||
{% if room_members and quota.get_left(len(room_members)) == 0 and (not order.room_confirmed) %}
|
||||
<p class="notice">⚠️ Your room contains {{len(room_members)}} people inside, but sadly there are no more {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} rooms. You need to add or remove people until you reach the size of an available room if you want to confirm it.</p>
|
||||
{% endif %}
|
||||
{# {% if room_members and quota.get_left(len(room_members)) == 0 and (not order.room_confirmed) %} #}
|
||||
{# <p class="notice">⚠️ Your room contains {{len(room_members)}} people inside, but sadly there are no more {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} rooms. You need to add or remove people until you reach the size of an available room if you want to confirm it.</p> #}
|
||||
{# {% endif %} #}
|
||||
|
||||
{# Show alert if room was not confirmed #}
|
||||
{% if order.room_id and not order.room_confirmed %}
|
||||
|
@ -15,7 +21,8 @@
|
|||
|
||||
{# Show notice if the room is confirmed #}
|
||||
{% if order.room_confirmed %}
|
||||
<p class="notice" style="background:#060">✅ Your <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room has been confirmed</p>
|
||||
{# <p class="notice" style="background:#060">✅ Your <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room has been confirmed</p> #}
|
||||
<p class="notice" style="background:#060">✅ Your <strong>{{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}}</strong> room has been confirmed</p>
|
||||
{% endif %}
|
||||
|
||||
{# Show roommates if room is set #}
|
||||
|
@ -24,13 +31,12 @@
|
|||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% for person in room_members %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
<div class="propic-container">
|
||||
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
|
||||
<img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
|
||||
</div>
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h3>{{person.ans('fursona_name')}}</h3>
|
||||
{% if person.code == order.room_id %}<p><strong style="color:#c6f">ROOM OWNER</strong></p>{% endif %}
|
||||
<p>{{person.ans('staff_title') if person.ans('staff_title') else ''}} {{'Fursuiter' if person.ans('is_fursuiter') != 'No'}}</p>
|
||||
<p>{{person.ans('staff_title') if person.ans('staff_title') else ''}} {{'Fursuiter' if person.is_fursuiter}}</p>
|
||||
{% if person.status == 'pending' %}
|
||||
<p><strong style="color:red;">UNPAID</strong></p>
|
||||
{% endif %}
|
||||
|
@ -42,7 +48,8 @@
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if order.room_id == order.code and not order.room_confirmed and len(room_members) < 5 %}
|
||||
{# {% if order.room_id == order.code and not order.room_confirmed and len(room_members) < 5%} #}
|
||||
{% if order.room_id == order.code and not order.room_confirmed and len(room_members) < order.room_person_no %}
|
||||
<div>
|
||||
<a href="javascript:document.getElementById('modal-roominvite').setAttribute('open', 'true');">
|
||||
<div class="propic-container">
|
||||
|
@ -72,14 +79,17 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<p class="grid">
|
||||
<p class="grid grid_2x2">
|
||||
{% if order.room_owner %}
|
||||
{% if len(room_members) == 1 and not order.room_confirmed %}
|
||||
<a href="/manage/room/delete" role="button">Delete room</a>
|
||||
{% endif %}
|
||||
|
||||
{% if not order.room_confirmed %}
|
||||
<a role="button" {% if not room.forbidden and quota.get_left(len(room_members)) > 0 %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room</a>
|
||||
{# <a role="button" {% if not room.forbidden and quota.get_left(len(room_members)) > 0 %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room</a> #}
|
||||
|
||||
<a style="grid-area: 1 / 1 / 2 / 2;" role="button" href="javascript:document.getElementById('modal-roomrename').setAttribute('open', 'true');">Rename room</a>
|
||||
<a style="grid-area: 1 / 2 / 2 / 3;" href="/manage/room/delete" role="button" {{'disabled' if (len(room_members) > 1) else ''}} >Delete room</a>
|
||||
<a style="grid-area: 2 / 1 / 3 / 3; display:block;" role="button" {% if not room.forbidden and len(room_members) == order.room_person_no %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}}</strong> room</a>
|
||||
{% else %}
|
||||
{# <a style="grid-area: 1 / 1 / 2 / 2;" role="button" href="javascript:navigator.share({title: 'Furizon room', text:'Viewing room {{order.room_name}}', url: `${window.location.protocol}//${window.location.host}/manage/room/view/{{order.code}}}`});">Share</a> #}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if order.room_id and not order.room_confirmed %}
|
||||
|
@ -100,8 +110,10 @@
|
|||
{% if person.status == 'pending' %}
|
||||
<td><strong style="color:red;">UNPAID</strong></td>
|
||||
{% endif %}
|
||||
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/approve/{{person.code}}">Approve</a></td>
|
||||
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/reject/{{person.code}}">Reject</a></td>
|
||||
{% if order.room_owner %}
|
||||
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/approve/{{person.code}}">Approve</a></td>
|
||||
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/reject/{{person.code}}">Reject</a></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</div>
|
||||
|
||||
|
@ -113,13 +125,13 @@
|
|||
{% endif %}
|
||||
|
||||
{# Room availability is always shown #}
|
||||
<h4>Room availability</h4>
|
||||
<table>
|
||||
{% for q in quota.data['results'] if 'Room' in q['name'] %}
|
||||
<tr {% if q['available_number'] == 0 %}style="text-decoration:line-through;"{% endif %}>
|
||||
<td>{{q['name']}}</td>
|
||||
<td>{{q['available_number']}} left</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{# <h4>Room availability</h4> #}
|
||||
{# <table> #}
|
||||
{# {% for q in quota.data['results'] if 'Room' in q['name'] %} #}
|
||||
{# <tr {% if q['available_number'] == 0 %}style="text-decoration:line-through;"{% endif %}> #}
|
||||
{# <td>{{q['name']}}</td> #}
|
||||
{# <td>{{q['available_number']}} left</td> #}
|
||||
{# </tr> #}
|
||||
{# {% endfor %} #}
|
||||
{# </table> #}
|
||||
</details>
|
||||
|
|
|
@ -29,21 +29,36 @@
|
|||
<table>
|
||||
<tr>
|
||||
<td>Room type</td>
|
||||
<td><strong>{{[None,'Single','Double','Triple','Quadruple','Quintuple'][len(room_members)]}} Room</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rooms left of this type</td>
|
||||
<td><strong>{{quota.get_left(len(room_members))}}</strong></td>
|
||||
{# <td><strong>{{[None,'Single','Double','Triple','Quadruple','Quintuple'][len(room_members)]}} Room</strong></td> #}
|
||||
<td><strong>{{[None,'Single','Double','Triple','Quadruple','Quintuple'][order.room_person_no]}} Room</strong></td>
|
||||
</tr>
|
||||
{# <tr> #}
|
||||
{# <td>Rooms left of this type</td> #}
|
||||
{# <td><strong>{{quota.get_left(len(room_members))}}</strong></td> #}
|
||||
{# </tr> #}
|
||||
</table>
|
||||
<footer>
|
||||
<a href="javascript:document.getElementById('modal-roomconfirm').removeAttribute('open')" role="button">Close</a>
|
||||
<a href="/manage/room/confirm" role="button">Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room</a>
|
||||
{# <a href="/manage/room/confirm" role="button">Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}}</strong> room</a> #}
|
||||
<a href="/manage/room/confirm" role="button">Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}}</strong> room</a>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
|
||||
<dialog id="modal-roomrename">
|
||||
<article>
|
||||
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
|
||||
<h3>Rename this room</h3>
|
||||
<p>Enter your room's new name!</p>
|
||||
<p>This name will be public and shown in the room list, so nothing offensive! :)</p>
|
||||
|
||||
<form method="POST" action="/manage/room/rename">
|
||||
<label for="name"></label>
|
||||
<input type="text" name="name" required minlength="4" maxlength="64" value="{{order.ans('room_name')}}"/>
|
||||
<input type="submit" value="Rename room" />
|
||||
</form>
|
||||
</article>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Furizon 2023 Carpooling{% endblock %}
|
||||
{% block title %}Furizon 2024 Carpooling{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<header>
|
||||
|
@ -42,13 +42,13 @@
|
|||
Day of departure
|
||||
|
||||
<select name="day_departure" id="day_departure">
|
||||
<option value="m29th" {{'selected' if order.carpooling_message.day_departure == 'm29th'}}>May 29th</option>
|
||||
<option value="m30th" {{'selected' if order.carpooling_message.day_departure == 'm30th'}}>May 30th</option>
|
||||
<option value="m31st" {{'selected' if order.carpooling_message.day_departure == 'm31st'}}>May 31st</option>
|
||||
<option value="j1st" {{'selected' if order.carpooling_message.day_departure == 'j1st'}}>June 1st</option>
|
||||
<option value="j2nd" {{'selected' if order.carpooling_message.day_departure == 'j2nd'}}>June 2nd</option>
|
||||
<option value="j3rd" {{'selected' if order.carpooling_message.day_departure == 'j3rd'}}>June 3rd</option>
|
||||
<option value="j4th" {{'selected' if order.carpooling_message.day_departure == 'j4th'}}>June 4th</option>
|
||||
<option value="j5th" {{'selected' if order.carpooling_message.day_departure == 'j5th'}}>June 5th</option>
|
||||
<option value="j6th" {{'selected' if order.carpooling_message.day_departure == 'j6th'}}>June 6th</option>
|
||||
<option value="j7th" {{'selected' if order.carpooling_message.day_departure == 'j7th'}}>June 7th</option>
|
||||
<option value="j8th" {{'selected' if order.carpooling_message.day_departure == 'j8th'}}>June 8th</option>
|
||||
<option value="j9th" {{'selected' if order.carpooling_message.day_departure == 'j9th'}}>June 9th</option>
|
||||
</select>
|
||||
</label>
|
||||
<textarea id="message" name="message" style="height:10em;" placeholder="Write here your message" required>{{order.carpooling_message.message}}</textarea>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<form id="intentFormAction" method="GET" action="">
|
||||
<dialog id="modalRoomconfirm">
|
||||
<article>
|
||||
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
|
||||
<h3 id="intentText">Confirm action</h3>
|
||||
<p id="intentDescription"></p>
|
||||
<div id="intentEditPanel"></div>
|
||||
<footer>
|
||||
<input id="intentSend" type="submit" value="Confirm" />
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
</form>
|
|
@ -0,0 +1,26 @@
|
|||
<!--By Drew tha woof-->
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<style media="all" type="text/css">
|
||||
* { color: #bbc6ce; }
|
||||
.body { width: 100%; margin: 0; background-color: #11191f; }
|
||||
.container { max-width: 40em; padding: 1em; margin: 0 auto; }
|
||||
.title { font-size: 1.75em; margin-bottom: 1.2em; color: #e1e6eb; margin-top: 0; font-family: sans-serif; }
|
||||
.main-content { margin-top: 0; font-style: normal; font-weight: 400; font-family: sans-serif;}
|
||||
.con-logo { height:3em;}
|
||||
.link { text-decoration: none; background-color: #1095c1; color: #fff; padding: 1em; border-radius: 5px; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<div class="body">
|
||||
<div class="container">
|
||||
<img src="https://reg.furizon.net/res/furizon.png" alt="con_logo" title="con_logo" class="con-logo">
|
||||
<div>
|
||||
<h2 class="title">{{title}}</h2>
|
||||
<p class="main-content">{{body}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</html>
|
|
@ -1,10 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Error {{exception.status_code}}{% endblock %}
|
||||
{% block title %}Error {{status_code}}{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<h1>{{exception.status_code}}</h1>
|
||||
<h1>{{status_code}}</h1>
|
||||
<p>{{exception}}</p>
|
||||
{% if exception.status_code == 409 %}
|
||||
{% if status_code == 409 %}
|
||||
<p>Retrying in 1 second...</p>
|
||||
<meta http-equiv="refresh" content="1">
|
||||
{% endif %}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Furizon 2024 Fursuitcount{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<header>
|
||||
<picture>
|
||||
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
|
||||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
<!--h2>Fursuit-count!</h2-->
|
||||
<p>Welcome to the fursuit-count page! Here you can see all of the fursuits that you'll find at Furizon!</p>
|
||||
{% for person in orders.values() if person.is_fursuiter%}
|
||||
{% if loop.first %}
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% endif %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic_fursuiter') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% if loop.last %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Furizon 2023 Karaoke Admin{% endblock %}
|
||||
{% block title %}Furizon 2024 Karaoke Admin{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<h1>Karaoke Admin</h1>
|
||||
|
|
|
@ -1,50 +1,99 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Furizon 2023 Nosecount{% endblock %}
|
||||
{% block title %}Furizon 2024 Nosecount{% endblock %}
|
||||
{% block head %}
|
||||
<meta property="og:title" content="Nose count - Furizon" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:alt" content="Furizon logo" />
|
||||
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
|
||||
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
|
||||
<meta property="og:description" content="Explore this year's rooms, find your friends and plan your meet-ups."/>
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
{% if order and order.isAdmin() %}
|
||||
<script src="/res/scripts/roomManager.js"></script>
|
||||
{% endif %}
|
||||
<header>
|
||||
<picture>
|
||||
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
|
||||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
|
||||
<p>Welcome to the nosecount page! Here you can see all of the available rooms at the convention, as well as the occupants currently staying in each room. Use this page to find your friends and plan your meet-ups.</p>
|
||||
{% for o in orders.values() %}
|
||||
{% if o.code == o.room_id and o.room_confirmed %}
|
||||
<h4 style="margin-top:1em;">{{o.room_name}}</h4>
|
||||
|
||||
{% if filtered and order %}
|
||||
{% for person in filtered.values() %}
|
||||
{% if loop.first %}
|
||||
<hr />
|
||||
<p>{{filter_header}}</p>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% endif %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% if loop.last %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% for o in orders.values() if (o.code == o.room_id and o.room_confirmed) %}
|
||||
{% if loop.first %}
|
||||
<hr />
|
||||
<h1>Confirmed rooms{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
|
||||
{% endif %}
|
||||
<h4 style="margin-top:1em;">
|
||||
<span>{{o.room_name}}</span>
|
||||
{% if order and order.isAdmin() %}
|
||||
<div class="room-actions">
|
||||
<a onclick="confirmAction('rename', this)" action="/manage/admin/room/rename/{{o.code}}"><img src="/res/icons/pencil.svg" class="icon" /><span>Rename</span></a>
|
||||
<a onclick="confirmAction('unconfirm', this)" action="/manage/admin/room/unconfirm/{{o.code}}"><img src="/res/icons/door_open.svg" class="icon" /><span>Unconfirm</span></a>
|
||||
<a class="act-del" onclick="confirmAction('delete', this)" action="/manage/admin/room/delete/{{o.code}}"><img src="/res/icons/delete.svg" class="icon" /><span>Delete</span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% for m in o.room_members %}
|
||||
{% if m in orders %}
|
||||
{% with person = orders[m] %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
<div class="propic-container">
|
||||
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
|
||||
<img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
|
||||
</div>
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed and len(o.room_members) > 1) %}
|
||||
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed) %}
|
||||
{% if loop.first %}
|
||||
<hr />
|
||||
<h1>Unconfirmed rooms</h1>
|
||||
<h1>Unconfirmed rooms{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
|
||||
<p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p>
|
||||
{% endif %}
|
||||
<h3>{{o.room_name}}</h3>
|
||||
<h4>
|
||||
<span>{{o.room_name}}</span>
|
||||
{% if o.room_person_no - len(o.room_members) > 0 %} <span class="nsc-room-counter"> - Remaining slots: {{o.room_person_no - len(o.room_members)}}</span> {% endif %}
|
||||
{% if order and order.isAdmin() %}
|
||||
<div class="room-actions">
|
||||
<a onclick="confirmAction('rename', this)" action="/manage/admin/room/rename/{{o.code}}"><img src="/res/icons/pencil.svg" class="icon" /><span>Rename</span></a>
|
||||
<a class="act-del" onclick="confirmAction('delete', this)" action="/manage/admin/room/delete/{{o.code}}"><img src="/res/icons/delete.svg" class="icon" /><span>Delete</span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h4>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% for m in o.room_members %}
|
||||
{% if m in orders %}
|
||||
{% with person = orders[m] %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
<div class="propic-container">
|
||||
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
|
||||
<img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
|
||||
</div>
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
@ -54,22 +103,54 @@
|
|||
{% if loop.last %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for person in orders.values() if (not person.room_id or len(person.room_members) == 1) and (not person.room_confirmed)%}
|
||||
{% for person in orders.values() if not person.room_id and (not person.room_confirmed) and not person.daily %}
|
||||
{% if loop.first %}
|
||||
<hr />
|
||||
<h1>Roomless furs</h1>
|
||||
<h1>Roomless furs{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
|
||||
<p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% endif %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
<div class="propic-container">
|
||||
<img class="propic propic-{{person.sponsorship}}" src="/res/propic/{{person.ans('propic') or 'default.png'}}" />
|
||||
<img class="propic-flag" src="/res/flags/{{person.country.lower()}}.svg" />
|
||||
</div>
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% if loop.last %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% for person in orders.values() if person.daily %}
|
||||
{% if loop.first %}
|
||||
<hr />
|
||||
<h1>Daily furs!{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
|
||||
<p>These furs will not stay in our hotels, but may be there with us just a few days!</p>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% endif %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% if loop.last %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<form id="intentFormAction" method="GET" action="">
|
||||
<dialog id="modalOrderEditDialog">
|
||||
<article>
|
||||
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
|
||||
<h3 id="intentText">Confirm room edit</h3>
|
||||
<p id="intentDescription"></p>
|
||||
<div id="intentEditPanel">
|
||||
<label for="name">Enter a new room name</label>
|
||||
<input id="intentRename" name="name" type="text" value="" maxlength="64"/>
|
||||
</div>
|
||||
<footer>
|
||||
<input id="intentSend" type="submit" value="Confirm" />
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
</form>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Furizon 2023 Nosecount{% endblock %}
|
||||
{% block title %}Furizon 2024 Nosecount{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<header>
|
||||
|
@ -8,7 +8,7 @@
|
|||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
<p class="notice" style="margin:2em 0;">⚠️ This privacy policy is a courtesy explanation of <a href="https://furizon.net/wp-content/uploads/2023/01/Regolamento-Furizon-Beyond-en-GB.pdf">the one you signed up when registering to the con</a></p>
|
||||
{# <p class="notice" style="margin:2em 0;">⚠️ This privacy policy is a courtesy explanation of <a href="https://furizon.net/wp-content/uploads/2023/01/Regolamento-Furizon-Beyond-en-GB.pdf">the one you signed up when registering to the con</a></p> #}
|
||||
<h1>Privacy policy of this private area</h1>
|
||||
<p>We collect only the data that is needed by law and to make sure that your convention experience is up to your expectations. Keep reading to know why and how we collect this data.</p>
|
||||
|
||||
|
@ -28,7 +28,6 @@
|
|||
<li>First three octets of your ip address</li>
|
||||
<li>List of status codes (404, 500, 403...) you encountered, and when you encountered them</li>
|
||||
</ul>
|
||||
<p>This info is collected through our first-party domain <em>y.foxo.me</em></p>
|
||||
|
||||
<h2>Backups and export of data</h2>
|
||||
<p>All data is stored in servers managed by Furizon APS, inside of facilities in the Italian territory. No data is exported outside of the italian territory. All communication uses end to end encryption.</p>
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Furizon 2024 Sponsorcount{% endblock %}
|
||||
{% block head %}
|
||||
<meta property="og:title" content="Sponsor count - Furizon" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:alt" content="Furizon logo" />
|
||||
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
|
||||
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
|
||||
<meta property="og:description" content="Thanks to all the amazing furs who decided to support us this year ❤️"/>
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<header>
|
||||
<picture>
|
||||
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
|
||||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
<h2 class="rainbow-text">Sponsor count!</h2>
|
||||
<p>Welcome to the sponsor-count page! This is the list of users that support us! Thanks really a lot to everyone ❤️</p>
|
||||
{% for person in orders.values() if person.sponsorship == "super"%}
|
||||
{% if loop.first %}
|
||||
<hr />
|
||||
<h1>Super sponsors!</h1>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% endif %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% if loop.last %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for person in orders.values() if person.sponsorship == "normal"%}
|
||||
{% if loop.first %}
|
||||
<hr />
|
||||
<h1>Sponsors</h1>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% endif %}
|
||||
<div style="margin-bottom: 1em;">
|
||||
{% with current=order, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% if loop.last %}</div>{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</main>
|
||||
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{room_data['name']}}{% endblock %}
|
||||
{% block head %}
|
||||
<!--Open Graph tags here-->
|
||||
<meta property="og:title" content="View room - Furizon" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:alt" content="View of a room" />
|
||||
<meta property="og:image" content="http://localhost:8188/{{preview}}" />
|
||||
<meta property="og:image:secure_url" content="http://localhost:8188/{{preview}}" />
|
||||
<meta property="og:image:width" content="{{230 * room_data['capacity'] + 130}}"/>
|
||||
<meta property="og:image:height" content="270"/>
|
||||
<meta property="og:description" content="Room {{room_data['name']}} has {{'been confirmed.' if room_data['confirmed'] else ('been filled.' if room_data['free_spots'] == 0 else (room_data['free_spots'] | string) + ' free spots out of ' + (room_data['capacity'] | string )) }}"/>
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<header>
|
||||
<picture>
|
||||
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
|
||||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;" onload="window.location.href = '/manage/nosecount/'">
|
||||
</picture>
|
||||
</header>
|
||||
</main>
|
||||
|
||||
{% endblock %}
|
|
@ -1,6 +1,16 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}{{order.name}}'s Booking{% endblock %}
|
||||
{% block head %}
|
||||
<!--Open Graph tags here-->
|
||||
<meta property="og:title" content="Furizon booking management" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:alt" content="Furizon's logo" />
|
||||
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
|
||||
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
|
||||
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
{% set locale = order.get_language() %}
|
||||
<main class="container">
|
||||
<header>
|
||||
<picture>
|
||||
|
@ -12,7 +22,7 @@
|
|||
<p>From here, you can easily manage all aspects of your booking, including composing your hotel room, designing your badge, and updating your payment information. Simply use the buttons below to navigate between the different sections.</p>
|
||||
<p>Buttons marked with ⚠️ require your attention</p>
|
||||
|
||||
<p>If you have any questions or issues while using this page, please don't hesitate to contact us for assistance. We look forward to seeing you at Furizon Riverside!</p>
|
||||
<p>If you have any questions or issues while using this page, please don't hesitate to contact us for assistance. We look forward to seeing you at Furizon Overlord!</p>
|
||||
<hr />
|
||||
<h2 id="info">Useful information</h2>
|
||||
<table>
|
||||
|
@ -24,13 +34,14 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th>When{{' (convention)' if order.has_early or order.has_late else ''}}?</th>
|
||||
<td>13 October → 15 October 2023</td></td>
|
||||
{# This should be early/late excluded! #}
|
||||
<td><img src="/res/icons/calendar.svg" class="icon" />4 June → 8 June 2024</td></td>
|
||||
</tr>
|
||||
{% if order.has_early or order.has_late %}
|
||||
<tr>
|
||||
<th>When (check-in)?</th>
|
||||
<td>
|
||||
{{('12' if order.has_early else '13')|safe}} October → {{('16' if order.has_late else '15')|safe}} October 2023
|
||||
{{('3' if order.has_early else '4')|safe}} June → {{('9' if order.has_late else '8')|safe}} June 2024
|
||||
{% if order.has_early %}
|
||||
<span class="tag">EARLY</span>
|
||||
{% endif %}
|
||||
|
@ -53,19 +64,36 @@
|
|||
{% if order.status == 'paid' and order.room_confirmed %}
|
||||
<br />
|
||||
<img src="/res/icons/pdf.svg" class="icon" />
|
||||
<a href="/manage/download_ticket?name=RIVERSIDE-{{order.code}}.pdf" target="_blank">Download ticket PDF</a>
|
||||
<a href="/manage/download_ticket?name=OVERLORD-{{order.code}}.pdf" target="_blank">Download ticket PDF</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if order.shuttle_bus %}
|
||||
<tr>
|
||||
<th>Shuttle</th>
|
||||
<td>
|
||||
<img src="/res/icons/bus.svg" class="icon" />
|
||||
{{order.shuttle_bus}}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
<h2>Manage your booking</h2>
|
||||
{% include 'blocks/payment.html' %}
|
||||
{% if order.position_positiontypeid not in ITEM_IDS['daily'] %}
|
||||
{% if not order.daily %}
|
||||
{% include 'blocks/room.html' %}
|
||||
{% endif %}
|
||||
{% include 'blocks/badge.html' %}
|
||||
|
||||
<details id="shuttle">
|
||||
<summary role="button"><img src="/res/icons/bus.svg" class="icon" />Shuttle</summary>
|
||||
<p><b>Due to the low number of requests, the shuttle service managed by Trentino Trasporti will not be available. Those who have purchased a bus ticket will be refunded directly by the transport company</b></p>
|
||||
<p>On the Furizon Telegram group, there is an active topic dedicated to car sharing, and the staff is available to look for custom alternative solutions. We apologize for the inconvenience.</p>
|
||||
<!--p>This year, a shuttle service operated by the tourism company of Val di Fiemme will be available. The shuttle service will consist of a bus serving the convention, with scheduled stops at major airports and train stations. More informations <a href="https://furizon.net/furizon-overlord/furizon-overlord-shuttle-bus/">in the dedicated page.</a></p>
|
||||
<p style="text-align:right;"><a href="{{LOCALES['shuttle_link_url'][locale]}}" target="_blank" role="button">Book now!</a></p-->
|
||||
</details>
|
||||
|
||||
<details id="barcard">
|
||||
<summary role="button"><img src="/res/icons/bar.svg" class="icon" />Barcard</summary>
|
||||
<p>This year's badges will be NFC-enabled and serve as a digital barcard, allowing you to load 'drinks' onto your badge and use it to purchase beverages at the bar without the need for physical cash or the risk of losing a paper barcard. The barcard system will be enabled closer to the convention, so you will have the opportunity to load your badge in advance and enjoy a convenient, cashless experience at the event. Keep an eye out for updates on when the system will be live and available for use.</p>
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Room Wizard{% endblock %}
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/res/styles/wizard.css">
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<script src="/res/scripts/wizardManager.js" type="text/javascript" defer="defer"></script>
|
||||
<header>
|
||||
<picture>
|
||||
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
|
||||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
<!--order = current order login
|
||||
unconfirmed_orders = all non confirmed rooms orders
|
||||
all_orders = all orders
|
||||
data = assigned rooms -->
|
||||
<h2>Review rooms <a href="#popover-empty-room-tip" onclick="document.querySelector('#popover-wizard-tip').showPopover()">?</a></h2>
|
||||
<div popover id="popover-wizard-tip">This is the preview page. Re-arrange users by dragging and dropping them in the rooms.<br>Once finished, scroll down to either <i>'Confirm'</i> changes or <i>'Undo'</i> them.</div>
|
||||
<hr>
|
||||
{% for room in data.items() %}
|
||||
{% if room[0] in all_orders %}
|
||||
{%with room_order = unconfirmed_orders[room[0]] %}
|
||||
<div class="room" id="room-{{room_order.code}}" room-type="{{room_order.bed_in_room}}" room-size="{{room_order.room_person_no - len(room_order.room_members)}}" current-size="{{len(room[1]['to_add'])}}">
|
||||
<h4 style="margin-top:1em;">
|
||||
<span>{{room_order.room_name if room_order.room_name else room[1]['room_name'] if room[1] and room[1]['room_name'] else ''}}</span>
|
||||
</h4>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% for m in room_order.room_members %}
|
||||
{% if m in all_orders %}
|
||||
{% with person = all_orders[m] %}
|
||||
<div class="edit-disabled" style="margin-bottom: 1em;">
|
||||
{% with current=None, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for m in room[1]['to_add'] %}
|
||||
{% if m in unconfirmed_orders %}
|
||||
{% with person = unconfirmed_orders[m] %}
|
||||
<div class="edit-drag" id="{{person.code}}" room-type="{{person.bed_in_room}}" style="margin-bottom: 1em;" draggable="true">
|
||||
{% with current=None, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="room" infinite="true" id="room-infinite" room-size="999999999" current-size="0">
|
||||
<h4 style="margin-top:1em;">Empty room <a href="#popover-empty-room-tip" onclick="document.querySelector('#popover-empty-room-tip').showPopover()">?</a></button></h4>
|
||||
<div popover id="popover-empty-room-tip">This is a placeholder room. Place users temporarily in order to free space and arrange rooms</div>
|
||||
<div class="grid people" style="padding-bottom:1em;"></div>
|
||||
</div>
|
||||
<a href="/manage/admin" role="button" title="Discard all changes and go back to the admin page">Undo</a>
|
||||
<a href="#" class="align-right" onclick="onSave()" role="button" title="Will try saving current changes.">Confirm changes</a>
|
||||
|
||||
<dialog id="modalConfirmDialog">
|
||||
<article>
|
||||
<a href="#close" id="modalClose" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
|
||||
<h3 id="intentText">Confirm arrangement?</h3>
|
||||
<p id="intentDescription">
|
||||
Roomless guests will be moved around existing rooms and newly generated ones.<br>
|
||||
This will also confirm all rooms.
|
||||
</p>
|
||||
<div popover id="popover-status"><span id="popover-status-text"></span></div>
|
||||
<footer>
|
||||
<button id="intentSend" onclick="submitData(this)">Confirm</button>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
|
||||
<script type="text/javascript">
|
||||
let saveData = JSON.parse('{{jsondata|safe}}');
|
||||
</script>
|
||||
|
||||
</main>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,339 @@
|
|||
from os.path import join
|
||||
from sanic import exceptions
|
||||
from config import *
|
||||
import httpx
|
||||
from messages import ROOM_ERROR_TYPES
|
||||
from email_util import send_unconfirm_message
|
||||
from sanic.response import text, html, redirect, raw
|
||||
from sanic.log import logger
|
||||
from metrics import *
|
||||
import pretixClient
|
||||
import traceback
|
||||
|
||||
METADATA_TAG = "meta_data"
|
||||
VARIATIONS_TAG = "variations"
|
||||
|
||||
QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.html
|
||||
"number": "N",
|
||||
"one_line_string": "S",
|
||||
"multi_line_string": "T",
|
||||
"boolean": "B",
|
||||
"choice_from_list": "C",
|
||||
"multiple_choice_from_list": "M",
|
||||
"file_upload": "F",
|
||||
"date": "D",
|
||||
"time": "H",
|
||||
"date_time": "W",
|
||||
"country_code": "CC",
|
||||
"telephone_number": "TEL"
|
||||
}
|
||||
TYPE_OF_QUESTIONS = {} # maps questionId -> type
|
||||
|
||||
async def load_questions() -> bool:
|
||||
global TYPE_OF_QUESTIONS
|
||||
# TYPE_OF_QUESTIONS.clear() It should not be needed
|
||||
logger.info("[QUESTIONS] Loading questions...")
|
||||
success = True
|
||||
try:
|
||||
p = 0
|
||||
while 1:
|
||||
p += 1
|
||||
res = await pretixClient.get(f"questions/?page={p}", expectedStatusCodes=[200, 404])
|
||||
if res.status_code == 404: break
|
||||
|
||||
data = res.json()
|
||||
for q in data['results']:
|
||||
TYPE_OF_QUESTIONS[q['id']] = q['type']
|
||||
except Exception:
|
||||
logger.warning(f"[QUESTIONS] Error while loading questions.\n{traceback.format_exc()}")
|
||||
success = False
|
||||
return success
|
||||
|
||||
async def load_items() -> bool:
|
||||
global ITEMS_ID_MAP
|
||||
global ITEM_VARIATIONS_MAP
|
||||
global CATEGORIES_LIST_MAP
|
||||
global ROOM_TYPE_NAMES
|
||||
logger.info("[ITEMS] Loading items...")
|
||||
success = True
|
||||
try:
|
||||
p = 0
|
||||
while 1:
|
||||
p += 1
|
||||
res = await pretixClient.get(f"items/?page={p}", expectedStatusCodes=[200, 404])
|
||||
if res.status_code == 404: break
|
||||
|
||||
data = res.json()
|
||||
for q in data['results']:
|
||||
# Map item id
|
||||
itemName = check_and_get_name ('item', q)
|
||||
if itemName and itemName in ITEMS_ID_MAP:
|
||||
ITEMS_ID_MAP[itemName] = q['id']
|
||||
# If item has variations, map them, too
|
||||
if itemName in ITEM_VARIATIONS_MAP and VARIATIONS_TAG in q:
|
||||
isBedInRoom = itemName == 'bed_in_room'
|
||||
for v in q[VARIATIONS_TAG]:
|
||||
variationName = check_and_get_name('variation', v)
|
||||
if variationName and variationName in ITEM_VARIATIONS_MAP[itemName]:
|
||||
ITEM_VARIATIONS_MAP[itemName][variationName] = v['id']
|
||||
if isBedInRoom and variationName in ITEM_VARIATIONS_MAP['bed_in_room']:
|
||||
roomName = v['name'] if 'name' in v and isinstance(v['name'], str) else None
|
||||
if not roomName and 'value' in v:
|
||||
roomName = v['value'][list(v['value'].keys())[0]]
|
||||
ROOM_TYPE_NAMES[v['id']] = roomName
|
||||
# Adds itself to the category list
|
||||
categoryName = check_and_get_category ('item', q)
|
||||
if not categoryName or q['id'] in CATEGORIES_LIST_MAP[categoryName]: continue
|
||||
CATEGORIES_LIST_MAP[categoryName].append(q['id'])
|
||||
if (EXTRA_PRINTS):
|
||||
logger.debug(f'Mapped Items: %s', ITEMS_ID_MAP)
|
||||
logger.debug(f'Mapped Variations: %s', ITEM_VARIATIONS_MAP)
|
||||
logger.debug(f'Mapped categories: %s', CATEGORIES_LIST_MAP)
|
||||
logger.debug(f'Mapped Rooms: %s', ROOM_TYPE_NAMES)
|
||||
except Exception:
|
||||
logger.warning(f"[ITEMS] Error while loading items.\n{traceback.format_exc()}")
|
||||
success = False
|
||||
return success
|
||||
|
||||
# Tries to get an item name from metadata. Prints a warning if an item has no metadata
|
||||
def check_and_get_name(type, q):
|
||||
itemName = extract_metadata_name(q)
|
||||
if not itemName and EXTRA_PRINTS:
|
||||
logger.warning('%s %s has not been mapped.', type, q['id'])
|
||||
return itemName
|
||||
|
||||
def check_and_get_category (type, q):
|
||||
categoryName = extract_category (q)
|
||||
if not categoryName and EXTRA_PRINTS:
|
||||
logger.warning('%s %s has no category set.', type, q['id'])
|
||||
return categoryName
|
||||
|
||||
# Checks if the item has specified metadata name
|
||||
def internal_name_check (toExtract, name):
|
||||
return toExtract and name and METADATA_TAG in toExtract and toExtract[METADATA_TAG][METADATA_NAME] == str(name)
|
||||
|
||||
# Returns the item_name metadata from the item or None if not defined
|
||||
def extract_metadata_name (toExtract):
|
||||
return extract_data(toExtract, [METADATA_TAG, METADATA_NAME])
|
||||
|
||||
# Returns the category_name metadata from the item or None if not defined
|
||||
def extract_category (toExtract):
|
||||
return extract_data(toExtract, [METADATA_TAG, METADATA_CATEGORY])
|
||||
|
||||
def extract_data (dataFrom, tags):
|
||||
data = dataFrom
|
||||
for t in tags:
|
||||
if t not in data: return None
|
||||
data = data[t]
|
||||
return data
|
||||
|
||||
def key_from_value(dict, value):
|
||||
return [k for k,v in dict.items() if v == value]
|
||||
|
||||
def sizeof_fmt(num, suffix="B"):
|
||||
for unit in ("", "K", "M", "G", "T", "P", "E", "Z"):
|
||||
if abs(num) < 1000.0:
|
||||
return f"{num:3.1f}{unit}{suffix}"
|
||||
num /= 1000.0
|
||||
return f"{num:.1f}Yi{suffix}"
|
||||
|
||||
async def get_order_by_code(request, code, throwException=False):
|
||||
res = await request.app.ctx.om.get_order(code=code)
|
||||
if not throwException:
|
||||
return res
|
||||
if res is None:
|
||||
raise exceptions.BadRequest(f"[getOrderByCode] Code {code} not found!")
|
||||
return res
|
||||
|
||||
async def get_people_in_room_by_code(request, code, om=None):
|
||||
if not om: om = request.app.ctx.om
|
||||
await om.update_cache()
|
||||
return list(filter(lambda rm: rm.room_id == code, om.cache.values()))
|
||||
|
||||
async def confirm_room_by_order(order, request):
|
||||
bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room
|
||||
room_members = []
|
||||
for m in order.room_members:
|
||||
if m == order.code:
|
||||
res = order
|
||||
else:
|
||||
res = await request.app.ctx.om.get_order(code=m)
|
||||
|
||||
if res.room_id != order.code:
|
||||
raise exceptions.BadRequest("Please contact support: some of the members in your room are actually somewhere else")
|
||||
|
||||
if res.status != 'paid':
|
||||
raise exceptions.BadRequest("Somebody hasn't paid.")
|
||||
|
||||
if res.bed_in_room != bed_in_room:
|
||||
raise exceptions.BadRequest("Somebody has a ticket for a different type of room!")
|
||||
|
||||
if res.daily:
|
||||
raise exceptions.BadRequest("Somebody in your room has a daily ticket!")
|
||||
|
||||
room_members.append(res)
|
||||
|
||||
|
||||
if len(room_members) != order.room_person_no:
|
||||
raise exceptions.BadRequest("The number of people in your room mismatches your type of ticket!")
|
||||
|
||||
for rm in room_members:
|
||||
await rm.edit_answer('room_id', order.code)
|
||||
await rm.edit_answer('room_confirmed', "True")
|
||||
await rm.edit_answer('pending_roommates', None)
|
||||
await rm.edit_answer('pending_room', None)
|
||||
|
||||
for rm in room_members:
|
||||
await rm.send_answers()
|
||||
|
||||
async def unconfirm_room_by_order(order, room_members=None, throw=True, request=None, om=None):
|
||||
if not om: om = request.app.ctx.om
|
||||
if not order.room_confirmed:
|
||||
if throw:
|
||||
raise exceptions.BadRequest("Room is not confirmed!")
|
||||
else:
|
||||
return
|
||||
|
||||
room_members = await get_people_in_room_by_code(request, order.code, om) if not room_members or len(room_members) == 0 else room_members
|
||||
for p in room_members:
|
||||
await p.edit_answer('room_confirmed', "False")
|
||||
await p.send_answers()
|
||||
|
||||
async def remove_members_from_room(order, removeMembers):
|
||||
didSomething = False
|
||||
for member in removeMembers:
|
||||
if (member in order.room_members):
|
||||
order.room_members.remove(member)
|
||||
didSomething = True
|
||||
if(didSomething):
|
||||
await order.edit_answer("room_members", ','.join(order.room_members))
|
||||
await order.send_answers()
|
||||
return didSomething
|
||||
|
||||
async def validate_rooms(request, rooms, om):
|
||||
logger.info('Validating rooms...')
|
||||
if not om: om = request.app.ctx.om
|
||||
|
||||
# rooms_to_unconfirm is the room that MUST be unconfirmed, room_with_errors is a less strict set containing all rooms with kind-ish errors
|
||||
rooms_to_unconfirm = []
|
||||
room_with_errors = []
|
||||
remove_members = []
|
||||
|
||||
# Validate rooms
|
||||
for order in rooms:
|
||||
result = await check_room(request, order, om)
|
||||
if(len(order.room_errors) > 0):
|
||||
room_with_errors.append(result)
|
||||
check = result[1]
|
||||
if check != None and check == False:
|
||||
rooms_to_unconfirm.append(result)
|
||||
|
||||
# End here if no room has failed check
|
||||
if len(room_with_errors) == 0:
|
||||
logger.info('[ROOM VALIDATION] Every room passed the check.')
|
||||
return
|
||||
|
||||
roomErrListSrts = []
|
||||
for fr in room_with_errors:
|
||||
for error in fr[0].room_errors:
|
||||
roomErrListSrts.append(f"[ROOM VALIDATION] [ERR] Parent room: {fr[0].code} {'C' if fr[0].room_confirmed else 'N'} | Order {error[0] if error[0] else '-----'} with code {error[1]}")
|
||||
logger.warning(f'[ROOM VALIDATION] Room validation failed for orders: \n%s', "\n".join(roomErrListSrts))
|
||||
|
||||
# Get confirmed rooms that fail validation
|
||||
failed_confirmed_rooms = list(filter(lambda fr: (fr[0].room_confirmed == True), rooms_to_unconfirm))
|
||||
|
||||
didSomething = False
|
||||
|
||||
if len(failed_confirmed_rooms) == 0:
|
||||
logger.info('[ROOM VALIDATION] No rooms to unconfirm.')
|
||||
else:
|
||||
didSomething = True
|
||||
logger.info(f"[ROOM VALIDATION] Trying to unconfirm {len(failed_confirmed_rooms)} rooms...")
|
||||
|
||||
# Try unconfirming them
|
||||
for rtu in failed_confirmed_rooms:
|
||||
order = rtu[0]
|
||||
member_orders = rtu[2]
|
||||
logger.warning(f"[ROOM VALIDATION] [UNCONFIRMING] Unconfirming room {order.code}...")
|
||||
|
||||
# Unconfirm and email users about the room
|
||||
if UNCONFIRM_ROOMS_ENABLE:
|
||||
await unconfirm_room_by_order(order, member_orders, False, None, om)
|
||||
|
||||
for r in rooms_to_unconfirm:
|
||||
order = r[0]
|
||||
removeMembers = r[3]
|
||||
if len(removeMembers) > 0:
|
||||
logger.warning(f"[ROOM VALIDATION] [REMOVING] Removing members '{','.join(removeMembers)}' from room {order.code}")
|
||||
|
||||
if UNCONFIRM_ROOMS_ENABLE:
|
||||
didSomething |= await remove_members_from_room(order, removeMembers)
|
||||
if(r not in failed_confirmed_rooms): failed_confirmed_rooms.append(r)
|
||||
|
||||
|
||||
if(didSomething):
|
||||
logger.info(f"[ROOM VALIDATION] Sending unconfirm notice to room members...")
|
||||
sent_count = 0
|
||||
# Send unconfirm notice via email
|
||||
for rtu in failed_confirmed_rooms:
|
||||
order = rtu[0]
|
||||
member_orders = rtu[2]
|
||||
try:
|
||||
if UNCONFIRM_ROOMS_ENABLE:
|
||||
await send_unconfirm_message(order, member_orders)
|
||||
sent_count += len(member_orders)
|
||||
except Exception as ex:
|
||||
if EXTRA_PRINTS: logger.exception(str(ex))
|
||||
logger.info(f"[ROOM VALIDATION] Sent {sent_count} emails")
|
||||
|
||||
|
||||
async def check_room(request, order, om=None):
|
||||
room_errors = []
|
||||
room_members = []
|
||||
remove_members = []
|
||||
use_cached = request == None
|
||||
if not om: om = request.app.ctx.om
|
||||
if not order or not order.room_id or order.room_id != order.code: return (order, False, room_members, remove_members)
|
||||
|
||||
# This is not needed anymore you buy tickets already
|
||||
#if quotas.get_left(len(order.room_members)) == 0:
|
||||
# raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
|
||||
allOk = True
|
||||
|
||||
bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room
|
||||
for m in order.room_members:
|
||||
if m == order.code:
|
||||
res = order
|
||||
else:
|
||||
res = await om.get_order(code=m, cached=use_cached)
|
||||
|
||||
# Room user in another room
|
||||
if res.room_id != order.code:
|
||||
room_errors.append((res.code, 'room_id_mismatch'))
|
||||
allOk = False
|
||||
|
||||
if res.status == 'canceled':
|
||||
room_errors.append((res.code, 'canceled'))
|
||||
remove_members.append(res.code)
|
||||
allOk = False
|
||||
elif res.status != 'paid':
|
||||
room_errors.append((res.code, 'unpaid'))
|
||||
|
||||
if res.bed_in_room != bed_in_room:
|
||||
room_errors.append((res.code, 'type_mismatch'))
|
||||
if order.room_confirmed:
|
||||
allOk = False
|
||||
|
||||
if res.daily:
|
||||
room_errors.append((res.code, 'daily'))
|
||||
if order.room_confirmed:
|
||||
allOk = False
|
||||
|
||||
room_members.append(res)
|
||||
|
||||
if len(room_members) != order.room_person_no and order.room_person_no != None and order.room_person_no >= 0:
|
||||
room_errors.append((None, 'capacity_mismatch'))
|
||||
if order.room_confirmed:
|
||||
allOk = False
|
||||
order.set_room_errors(room_errors)
|
||||
return (order, allOk, room_members, remove_members)
|