Auto Id Indexing + Admin panel + Propic upload checks + Room editing

Added auto indexing by metadata, instead of ID matching
Added checks to propic image uploading
Added an admin page, featuring:
 - Manual cache clearing
 Added admin controls in nosecount to unconfirm / rename or delete the room

Co-authored-by: Luca Sorace "Stranck" <stranck@users.noreply.github.com>
This commit is contained in:
Andrea 2024-01-08 22:02:27 +01:00
parent 01da35560f
commit 53232de597
24 changed files with 559 additions and 87 deletions

72
admin.py Normal file
View File

@ -0,0 +1,72 @@
from email.mime.text import MIMEText
from sanic import response, redirect, Blueprint, exceptions
from config import *
from utils import *
from ext import *
import sqlite3
import smtplib
import random
import string
import httpx
import json
bp = Blueprint("admin", url_prefix="/manage/admin")
def credentialsCheck (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.isAdmin() : raise exceptions.Forbidden("Birichino :)")
@bp.get('/cache/clear')
async def clearCache(request, order:Order):
credentialsCheck(request, order)
await request.app.ctx.om.fill_cache()
return redirect(f'/manage/admin')
@bp.get('/room/unconfirm/<code>')
async def unconfirmRoom(request, code, order:Order):
credentialsCheck(request, order)
dOrder = await getOrderByCode_safe(request, code)
if(not dOrder.room_confirmed):
raise exceptions.BadRequest("Room is not confirmed!")
ppl = getPeopleInRoomByRoomId(request, code)
for p in ppl:
await p.edit_answer('room_confirmed', "False")
await p.send_answers()
return redirect(f'/manage/nosecount')
@bp.get('/room/delete/<code>')
async def deleteRoom(request, code, order:Order):
credentialsCheck(request, order)
dOrder = await getOrderByCode_safe(request, code)
ppl = getPeopleInRoomByRoomId(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 renameRoom(request, code, order:Order):
credentialsCheck(request, order)
dOrder = await getOrderByCode_safe(request, code)
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')

22
app.py
View File

@ -31,8 +31,9 @@ from stats import bp as stats_bp
from api import bp as api_bp from api import bp as api_bp
from carpooling import bp as carpooling_bp from carpooling import bp as carpooling_bp
from checkin import bp as checkin_bp from checkin import bp as checkin_bp
from admin import bp as admin_bp
app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp]) app.blueprint([room_bp, karaoke_bp, propic_bp, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp, admin_bp])
@app.exception(exceptions.SanicException) @app.exception(exceptions.SanicException)
async def clear_session(request, exception): async def clear_session(request, exception):
@ -40,8 +41,8 @@ async def clear_session(request, exception):
r = html(tpl.render(exception=exception)) r = html(tpl.render(exception=exception))
if exception.status_code == 403: if exception.status_code == 403:
del r.cookies["foxo_code"] r.delete_cookie("foxo_code")
del r.cookies["foxo_secret"] r.delete_cookie("foxo_secret")
return r return r
@app.before_server_start @app.before_server_start
@ -63,8 +64,12 @@ async def main_start(*_):
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True) app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
app.ctx.tpl.globals.update(time=time) app.ctx.tpl.globals.update(time=time)
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE) app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
app.ctx.tpl.globals.update(ITEM_IDS=ITEM_IDS) 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(ROOM_TYPE_NAMES=ROOM_TYPE_NAMES)
app.ctx.tpl.globals.update(PROPIC_MIN_SIZE=PROPIC_MIN_SIZE)
app.ctx.tpl.globals.update(PROPIC_MAX_SIZE=PROPIC_MAX_SIZE)
app.ctx.tpl.globals.update(PROPIC_MAX_FILE_SIZE=sizeof_fmt(PROPIC_MAX_FILE_SIZE))
app.ctx.tpl.globals.update(int=int) app.ctx.tpl.globals.update(int=int)
app.ctx.tpl.globals.update(len=len) app.ctx.tpl.globals.update(len=len)
@ -156,6 +161,15 @@ async def download_ticket(request, order: Order):
raise exceptions.SanicException("You can download your ticket only after the order has been confirmed and paid. Try later!", status_code=400) raise exceptions.SanicException("You can download your ticket only after the order has been confirmed and paid. Try later!", status_code=400)
return raw(res.content, content_type='application/pdf') return raw(res.content, content_type='application/pdf')
@app.route("/manage/admin")
async def admin(request, order: Order):
await request.app.ctx.om.updateCache()
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.isAdmin(): raise exceptions.Forbidden("Birichino :)")
tpl = app.ctx.tpl.get_template('admin.html')
return html(tpl.render(order=order))
@app.route("/manage/logout") @app.route("/manage/logout")
async def logour(request): async def logour(request):

View File

@ -8,47 +8,98 @@ base_url = "http://urlllllllllllllllllllll/api/v1/"
base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/" base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/"
PROPIC_DEADLINE = 9999999999 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)
FILL_CACHE = True FILL_CACHE = True
CACHE_EXPIRE_TIME = 60 * 60 * 4 CACHE_EXPIRE_TIME = 60 * 60 * 4
DEV_MODE = True DEV_MODE = True
ITEM_IDS = { # Metadata property for item-id mapping
'ticket': [126, 127, 155], METADATA_NAME = "item_name"
'membership_card': [128,], # Metadata property for internal category mapping (not related to pretix's category)
'sponsorship': [55, 56], # first one = normal, second = super METADATA_CATEGORY = "category_name"
'early_arrival': [133],
'late_departure': [134], # Maps Products metadata name <--> ID
'room': 135, ITEMS_ID_MAP = {
'bed_in_room': 153, 'early_bird_ticket': 126,
'daily': 162, 'regular_ticket': 127,
'daily_addons': [163, 164, 165, 166] #This should be in date order. If there are holes in the daily-span, insert an unexisting id 'staff_ticket': 155,
'daily_ticket': 162,
'sponsorship_item': 129,
'early_arrival_admission': 133,
'late_departure_admission': 134,
'membership_card_item': 128,
'bed_in_room': 153,
'room_type': 135,
'room_guest': 136,
'daily_1': 163,
'daily_2': 164,
'daily_3': 165,
'daily_4': 166,
'daily_5': None
} }
# Maps Products' variants metadata name <--> ID
ITEM_VARIATIONS_MAP = {
'sponsorship_item': {
'sponsorship_item_normal': 55,
'sponsorship_item_super': 56
},
'bed_in_room': {
'bed_in_room_main_1': 83,
'bed_in_room_main_2': 67,
'bed_in_room_main_3': 68,
'bed_in_room_main_4': 69,
'bed_in_room_main_5': 70,
'bed_in_room_overflow1_2': 75,
},
'room_type': {
'single': 57,
'double': 58,
'triple': 59,
'quadruple': 60,
'quintuple': 61
},
'room_guest': {
'single': 57,
'double': 58,
'triple': 59,
'quadruple': 60,
'quintuple': 61
}
}
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. # Create a bunch of "room" items which will get added to the order once somebody gets a room.
# Map variationId -> numberOfPeopleInRoom # Map item_name -> room capacity
ROOM_MAP = { ROOM_CAPACITY_MAP = {
# SACRO CUORE # SACRO CUORE
83: 1, 'bed_in_room_main_1': 1,
67: 2, 'bed_in_room_main_2': 2,
68: 3, 'bed_in_room_main_3': 3,
69: 4, 'bed_in_room_main_4': 4,
70: 5, 'bed_in_room_main_5': 5,
# OVERFLOW 1 # OVERFLOW 1
75: 2 'bed_in_room_overflow1_2': 2,
} }
ROOM_TYPE_NAMES = { # Autofilled
83: "Park Hotel Sacro Cuore (main hotel) - Single", ROOM_TYPE_NAMES = { }
67: "Park Hotel Sacro Cuore (main hotel) - Double",
68: "Park Hotel Sacro Cuore (main hotel) - Triple",
69: "Park Hotel Sacro Cuore (main hotel) - Quadruple",
70: "Park Hotel Sacro Cuore (main hotel) - Quintuple",
# OVERFLOW 1
75: "Hotel San Valier (overflow hotel) - Double"
}
# This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id. # This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id.
TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'

23
connector.py Normal file
View File

@ -0,0 +1,23 @@
# 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
import logging
log = logging.getLogger()
def checkConfig():
if (not DEV_MODE) and DUMMY_DATA:
log.warn('It is strongly unadvised to use dummy data in production')
def getOrders(page):
return None
def getOrder(code):
return None

View File

@ -8,7 +8,7 @@ bp = Blueprint("export", url_prefix="/manage/export")
@bp.route("/export.csv") @bp.route("/export.csv")
async def export_csv(request, order: Order): 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 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 :)") if not order.isAdmin(): raise exceptions.Forbidden("Birichino :)")
page = 0 page = 0
orders = {} orders = {}

40
ext.py
View File

@ -45,7 +45,7 @@ class Order:
self.country = idata['country'] self.country = idata['country']
for p in self.data['positions']: 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_id = p['id']
self.position_positionid = p['positionid'] self.position_positionid = p['positionid']
self.position_positiontypeid = p['item'] self.position_positiontypeid = p['item']
@ -55,32 +55,33 @@ class Order:
self.answers[i]['answer'] = "file:keep" self.answers[i]['answer'] = "file:keep"
self.barcode = p['secret'] self.barcode = p['secret']
self.checked_in = bool(p['checkins']) self.checked_in = bool(p['checkins'])
if p['item'] == ITEM_IDS['daily']:
self.daily = True
if p['item'] in ITEM_IDS['daily_addons']: if p['item'] in CATEGORIES_LIST_MAP['dailys']:
self.daily = True self.daily = True
self.dailyDays.append(ITEM_IDS['daily_addons'].index(p['item'])) 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 self.has_card = True
if p['item'] in ITEM_IDS['sponsorship']: if p['item'] == ITEMS_ID_MAP['sponsorship_item']:
self.sponsorship = 'normal' if p['variation'] == ITEMS_IDS['sponsorship'][0] else 'super' sponsorshipType = keyFromValue(ITEM_VARIATIONS_MAP['sponsorship_item'], p['variation'])
self.sponsorship = sponsorshipType[0].replace ('sponsorship_item_', '') if len(sponsorshipType) > 0 else None
if p['attendee_name']: if p['attendee_name']:
self.first_name = p['attendee_name_parts']['given_name'] self.first_name = p['attendee_name_parts']['given_name']
self.last_name = p['attendee_name_parts']['family_name'] self.last_name = p['attendee_name_parts']['family_name']
if p['item'] == ITEM_IDS['early_arrival']: if p['item'] == ITEMS_ID_MAP['early_arrival_admission']:
self.has_early = True self.has_early = True
if p['item'] == ITEM_IDS['late_departure']: if p['item'] == ITEMS_ID_MAP['late_departure_admission']:
self.has_late = True self.has_late = True
if p['item'] == ITEM_IDS['bed_in_room']: if p['item'] == ITEMS_ID_MAP['bed_in_room']:
roomTypeLst = keyFromValue(ITEM_VARIATIONS_MAP['bed_in_room'], p['variation'])
roomTypeId = roomTypeLst[0] if len(roomTypeLst) > 0 else None
self.bed_in_room = p['variation'] self.bed_in_room = p['variation']
self.room_person_no = ROOM_MAP[self.bed_in_room] if self.bed_in_room in ROOM_MAP else None self.room_person_no = ROOM_CAPACITY_MAP[roomTypeId] if roomTypeId in ROOM_CAPACITY_MAP else None
self.total = float(data['total']) self.total = float(data['total'])
self.fees = 0 self.fees = 0
@ -96,6 +97,8 @@ class Order:
self.payment_provider = data['payment_provider'] self.payment_provider = data['payment_provider']
self.comment = data['comment'] self.comment = data['comment']
self.phone = data['phone'] self.phone = data['phone']
self.loadAns()
def loadAns(self):
self.shirt_size = self.ans('shirt_size') self.shirt_size = self.ans('shirt_size')
self.is_artist = True if self.ans('is_artist') != 'No' else False self.is_artist = True if self.ans('is_artist') != 'No' else False
self.is_fursuiter = True if self.ans('is_fursuiter') != 'No' else False self.is_fursuiter = True if self.ans('is_fursuiter') != 'No' else False
@ -122,8 +125,8 @@ class Order:
self.nfc_id = self.ans('nfc_id') self.nfc_id = self.ans('nfc_id')
self.can_scan_nfc = True if self.ans('can_scan_nfc') != 'No' else False self.can_scan_nfc = True if self.ans('can_scan_nfc') != 'No' else False
self.actual_room = self.ans('actual_room') self.actual_room = self.ans('actual_room')
self.staff_role = self.ans('staff_role')
self.telegram_username = self.ans('telegram_username').strip('@') if self.ans('telegram_username') else None self.telegram_username = self.ans('telegram_username').strip('@') if self.ans('telegram_username') else None
def __getitem__(self, var): def __getitem__(self, var):
return self.data[var] return self.data[var]
@ -135,6 +138,12 @@ class Order:
return bool(a['answer'] == 'True') return bool(a['answer'] == 'True')
return a['answer'] return a['answer']
return None return None
def isBadgeValid (self):
return self.ans('propic') and (not self.is_fursuiter or self.ans('propic_fursuiter'))
def isAdmin (self):
return self.code in ADMINS or self.staff_role in ADMINS_PRETIX_ROLE_NAMES
async def edit_answer_fileUpload(self, name, fileName, mimeType, data : bytes): async def edit_answer_fileUpload(self, name, fileName, mimeType, data : bytes):
if(mimeType != None and data != None): if(mimeType != None and data != None):
@ -147,6 +156,7 @@ class Order:
await self.edit_answer(name, res['id']) await self.edit_answer(name, res['id'])
else: else:
await self.edit_answer(name, None) await self.edit_answer(name, None)
self.loadAns()
async def edit_answer(self, name, new_answer): async def edit_answer(self, name, new_answer):
found = False found = False
@ -168,7 +178,6 @@ class Order:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
res = await client.get(join(base_url_event, 'questions/'), headers=headers) res = await client.get(join(base_url_event, 'questions/'), headers=headers)
res = res.json() res = res.json()
for r in res['results']: for r in res['results']:
if r['identifier'] != name: continue if r['identifier'] != name: continue
@ -178,6 +187,7 @@ class Order:
'answer': new_answer, 'answer': new_answer,
'options': r['options'] 'options': r['options']
}) })
self.loadAns()
async def send_answers(self): async def send_answers(self):
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
@ -204,6 +214,7 @@ class Order:
self.pending_update = False self.pending_update = False
self.time = -1 self.time = -1
self.loadAns()
@dataclass @dataclass
class Quotas: class Quotas:
@ -254,6 +265,7 @@ class OrderManager:
self.order_list.remove(code) self.order_list.remove(code)
async def fill_cache(self): async def fill_cache(self):
await loadItems()
await loadQuestions() await loadQuestions()
self.empty() self.empty()
p = 0 p = 0

View File

@ -10,7 +10,7 @@ bp = Blueprint("karaoke", url_prefix="/manage/karaoke")
@bp.get("/admin") @bp.get("/admin")
async def show_songs(request, order: Order): async def show_songs(request, order: Order):
if order.code not in ADMINS: if not order.isAdmin():
raise exceptions.Forbidden("Birichino") raise exceptions.Forbidden("Birichino")
orders = [x for x in request.app.ctx.om.cache.values() if x.karaoke_songs] orders = [x for x in request.app.ctx.om.cache.values() if x.karaoke_songs]
@ -28,7 +28,7 @@ async def show_songs(request, order: Order):
@bp.post("/approve") @bp.post("/approve")
async def approve_songs(request, order: Order): async def approve_songs(request, order: Order):
if order.code not in ADMINS: if not order.isAdmin():
raise exceptions.Forbidden("Birichino") raise exceptions.Forbidden("Birichino")
for song in request.form: 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 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") raise exceptions.Forbidden("Birichino")
songname = unquote(songname) songname = unquote(songname)

View File

@ -6,14 +6,19 @@ from PIL import Image
from io import BytesIO from io import BytesIO
from hashlib import sha224 from hashlib import sha224
from time import time from time import time
import os
bp = Blueprint("propic", url_prefix="/manage/propic") bp = Blueprint("propic", url_prefix="/manage/propic")
async def resetDefaultPropic(request, order: Order, isFursuiter, sendAnswer=True): async def resetDefaultPropic(request, order: Order, isFursuiter, sendAnswer=True):
s = "_fursuiter" if isFursuiter else "" s = "_fursuiter" if isFursuiter else ""
print("Resetting default propic") if (DEV_MODE):
print("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: with open("res/propic/default.png", "rb") as f:
data = f.read() data = f.read()
f.close()
os.remove(f"res/propic/{order.ans(f'propic{s}')}") # converted file
os.remove(f"res/propic/{order.ans(f'propic{s}').split(".jpg")[0]}_original.jpg") # original file
await order.edit_answer_fileUpload(f'propic{s}_file', f'propic{s}_file_{order.code}_default.png', 'image/png', data) await order.edit_answer_fileUpload(f'propic{s}_file', f'propic{s}_file_{order.code}_default.png', 'image/png', data)
if(sendAnswer): if(sendAnswer):
await order.send_answers() await order.send_answers()
@ -41,17 +46,30 @@ async def upload_propic(request, order: Order):
if not body[0].body: continue if not body[0].body: continue
h = sha224(body[0].body).hexdigest()[:32] # Check max file size
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'))
h = sha224(body[0].body).hexdigest()[:32]
errorDetails = ''
try: try:
img = Image.open(BytesIO(body[0].body)) img = Image.open(BytesIO(body[0].body))
if(img.size[0] > 2048 or img.size[1] > 2048):
raise exceptions.BadRequest("Maximum allowed dimensions: 2048x2048")
with open(f"res/propic/{fn}_{order.code}_original", "wb") as f:
f.write(body[0].body)
width, height = img.size 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.jpg", "wb") as f:
f.write(body[0].body)
f.flush()
f.close()
aspect_ratio = width/height aspect_ratio = width/height
if aspect_ratio > 1: if aspect_ratio > 1:
crop_amount = (width - height) / 2 crop_amount = (width - height) / 2
@ -61,19 +79,22 @@ async def upload_propic(request, order: Order):
img = img.crop((0, crop_amount, width, height - crop_amount)) img = img.crop((0, crop_amount, width, height - crop_amount))
img = img.convert('RGB') img = img.convert('RGB')
width, height = img.size
img.thumbnail((512,512)) img.thumbnail((512,512))
imgBytes = BytesIO() imgBytes = BytesIO()
img.save(imgBytes, format='jpeg') img.save(imgBytes, format='jpeg')
imgBytes = imgBytes.getvalue() imgBytes = imgBytes.getvalue()
with open(f"res/propic/{fn}_{order.code}_{h}.jpg", "wb") as f: with open(f"res/propic/{fn}_{order.code}_{h}.jpg", "wb") as f:
f.write(imgBytes) f.write(imgBytes)
f.flush()
f.close()
await order.edit_answer_fileUpload(f'{fn}_file', f'{fn}_file_{order.code}_{h}.jpg', 'image/jpeg', imgBytes) await order.edit_answer_fileUpload(f'{fn}_file', f'{fn}_file_{order.code}_{h}.jpg', 'image/jpeg', imgBytes)
except Exception: except Exception:
import traceback import traceback
print(traceback.format_exc()) if DEV_MODE: print(traceback.format_exc())
raise exceptions.BadRequest("The image you uploaded is not valid.") raise exceptions.BadRequest(errorDetails if errorDetails else "The image you uploaded is not valid.")
else: else:
await order.edit_answer(fn, f"{fn}_{order.code}_{h}.jpg") await order.edit_answer(fn, f"{fn}_{order.code}_{h}.jpg")

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

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

After

Width:  |  Height:  |  Size: 300 B

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

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

After

Width:  |  Height:  |  Size: 399 B

BIN
res/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

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

After

Width:  |  Height:  |  Size: 310 B

View File

@ -0,0 +1,28 @@
function confirmAction (intent, sender) {
if (['rename', 'unconfirm', 'delete'].includes (intent) == false) return
let href = sender.getAttribute('action')
let intentTitle = document.querySelector("#intentText")
let intentEdit = document.querySelector("#intentRename")
let intentEditPanel = document.querySelector("#intentEditPanel")
let intentFormAction = document.querySelector("#intentFormAction")
let intentSend = document.querySelector("#intentSend")
// Resetting ui
intentEdit.setAttribute('required', false)
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)
intentFormAction.setAttribute('method', 'POST')
break
case 'unconfirm':
break
case 'delete':
break
}
document.getElementById('modalRoomconfirm').setAttribute('open', 'true');
}

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

@ -0,0 +1,18 @@
div.room-actions {
float: right;
}
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);
}

View File

@ -1,5 +1,7 @@
/* Other blocks' styles */ /* Other blocks' styles */
@import url('propic.css'); @import url('propic.css');
@import url('admin.css');
@import url('room.css');
summary:has(span.status) { summary:has(span.status) {
background-color: #ffaf0377; background-color: #ffaf0377;

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

@ -0,0 +1,14 @@
span.nsc-room-counter {
font-size: medium;
color: var(--color);
}
#intentFormAction #intentText {
text-transform: capitalize;
}
@media only screen and (max-width: 500px) {
div.room-actions a>span {
display: none;
}
}

34
room.py
View File

@ -49,7 +49,7 @@ async def delete_room(request, order: Order):
if not order: if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if order.room_id != order.code: if order.room_owner:
raise exceptions.BadRequest("You are not allowed to delete room of others.") raise exceptions.BadRequest("You are not allowed to delete room of others.")
if order.ans('room_confirmed'): if order.ans('room_confirmed'):
@ -176,6 +176,9 @@ async def cancel_request(request, order: Order):
async def approve_roomreq(request, code, order: Order): async def approve_roomreq(request, code, order: Order):
if not order: if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if not order.room_owner:
raise exceptions.BadRequest("You are not the owner of the room!")
if not code in order.pending_roommates: if not code in order.pending_roommates:
raise exceptions.BadRequest("You cannot accept people that didn't request to join your room") raise exceptions.BadRequest("You cannot accept people that didn't request to join your room")
@ -230,6 +233,9 @@ async def leave_room(request, order: Order):
async def reject_roomreq(request, code, order: Order): async def reject_roomreq(request, code, order: Order):
if not order: if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!") raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if not order.room_owner:
raise exceptions.BadRequest("You are not the owner of the room!")
if not code in order.pending_roommates: if not code in order.pending_roommates:
raise exceptions.BadRequest("You cannot reject people that didn't request to join your room") raise exceptions.BadRequest("You cannot reject people that didn't request to join your room")
@ -252,6 +258,32 @@ async def reject_roomreq(request, code, order: Order):
await order.send_answers() await order.send_answers()
return redirect('/manage/welcome') return redirect('/manage/welcome')
@bp.post("/rename")
async def rename_room(request, order: Order):
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if not order.room_owner:
raise exceptions.BadRequest("You are not the owner of the room!")
if not order.room_id:
raise exceptions.BadRequest("Try joining a room before renaming it.")
if order.room_confirmed:
raise exceptions.BadRequest("You can't rename a confirmed room!")
if order.room_id != order.code:
raise exceptions.BadRequest("You are not allowed to rename rooms of others.")
name = request.form.get('name')
if len(name) > 64 or len(name) < 4:
raise exceptions.BadRequest("Your room name is invalid. Please try another one.")
await order.edit_answer("room_name", name)
await order.send_answers()
return redirect('/manage/welcome')
@bp.route("/confirm") @bp.route("/confirm")
async def confirm_room(request, order: Order, quotas: Quotas): async def confirm_room(request, order: Order, quotas: Quotas):

18
tpl/admin.html Normal file
View File

@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}Admin panel{% 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>
<!-- Quick controls -->
<h2>Admin panel</h2>
<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 href="/manage/nosecount" role="button" title="Reload the orders' data and the re-sync items' indexes from pretix">Manage rooms</a>
<hr>
</main>
{% endblock %}

View File

@ -7,6 +7,7 @@
<meta name="supported-color-schemes" content="light dark"> <meta name="supported-color-schemes" content="light dark">
<link rel="stylesheet" href="/res/pico.min.css"> <link rel="stylesheet" href="/res/pico.min.css">
<link rel="stylesheet" href="/res/styles/base.css"> <link rel="stylesheet" href="/res/styles/base.css">
<link rel="icon" type="image/x-icon" href="/res/icons/favicon.ico">
<style> <style>
.people div {text-align:center;} .people div {text-align:center;}
.people h3, .people p, .people h5 {margin:0;} .people h3, .people p, .people h5 {margin:0;}
@ -99,6 +100,7 @@
{% if order %} {% if order %}
<a href="/manage/carpooling">Carpooling</a> <a href="/manage/carpooling">Carpooling</a>
<a style="float:right;" href="/manage/logout">Logout</a> <a style="float:right;" href="/manage/logout">Logout</a>
{% if order.isAdmin() %}<a style="float:right;" href="/manage/admin">Admin panel</a>{% endif %}
<a style="float:right;color: #b1b1b1;">Logged in as <i>{{order.ans('fursona_name')}}</i></a> <a style="float:right;color: #b1b1b1;">Logged in as <i>{{order.ans('fursona_name')}}</i></a>
{% endif %} {% endif %}
<br clear="both" /> <br clear="both" />

View File

@ -1,5 +1,5 @@
<details id="badge"> <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 #} {# Badge is always shown #}
<h2>Badge</h2> <h2>Badge</h2>
{% if order.propic_locked %} {% if order.propic_locked %}
@ -11,23 +11,23 @@
<form method="POST" enctype="multipart/form-data" action="/manage/propic/upload"> <form method="POST" enctype="multipart/form-data" action="/manage/propic/upload">
<div class="grid" style="text-align:center;margin-bottom:1em;"> <div class="grid" style="text-align:center;margin-bottom:1em;">
<div> <div>
{% if not order.ans('propic') %} {% with order=order, imgSrc='/res/propic/' + (order.ans('propic') or 'default.png'), effects = false %}
<input type="file" value="" accept="image/jpeg,image/png" name="propic" />
{% endif %}
{% with order=order, imgSrc='/res/propic/' + (order.ans('propic') or 'default.png'), effects = true %}
{% include 'blocks/propic.html' %} {% include 'blocks/propic.html' %}
{% endwith %} {% endwith %}
<p>Normal Badge</p> <p>Normal Badge</p>
{% if not order.ans('propic') %}
<input type="file" value="" accept="image/jpeg,image/png" name="propic" />
{% endif %}
</div> </div>
{% if order.is_fursuiter %} {% if order.is_fursuiter %}
<div> <div>
{% if not order.ans('propic_fursuiter') %} {% with order=order, imgSrc='/res/propic/' + (order.ans('propic_fursuiter') or 'default.png'), effects = false %}
<input type="file" value="" accept="image/jpeg,image/png" name="propic_fursuiter" />
{% endif %}
{% with order=order, imgSrc='/res/propic/' + (order.ans('propic_fursuiter') or 'default.png'), effects = true %}
{% include 'blocks/propic.html' %} {% include 'blocks/propic.html' %}
{% endwith %} {% endwith %}
<p>Fursuit Badge</p> <p>Fursuit Badge</p>
{% if not order.ans('propic_fursuiter') %}
<input type="file" value="" accept="image/jpeg,image/png" name="propic_fursuiter" />
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>
@ -36,7 +36,8 @@
<p class="notice">⚠️ The deadline to upload pictures for the badge has expired. For last-minute changes, please contact the support over at <a href="mailto:info@furizon.net">info@furizon.net</a>. If your badge has been printed already, changing it will incur in a 2€ fee. You can also get extra badges at the reception for the same price. If you upload a propic now, it might not be printed on time.</p> <p class="notice">⚠️ The deadline to upload pictures for the badge has expired. For last-minute changes, please contact the support over at <a href="mailto:info@furizon.net">info@furizon.net</a>. If your badge has been printed already, changing it will incur in a 2€ fee. You can also get extra badges at the reception for the same price. If you upload a propic now, it might not be printed on time.</p>
{% else %} {% else %}
<p><em> <p><em>
Min size: 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. Badge photos must clearly show the fursona/fursuit head.<br />Memes and low quality images will be removed and may limit your ability to upload pics in the future.
</em></p> </em></p>
{% endif %} {% endif %}

View File

@ -1,5 +1,5 @@
<details id="room"> <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> <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> <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><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> <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>
@ -77,13 +77,17 @@
<p class="grid"> <p class="grid">
{% if order.room_owner %} {% if order.room_owner %}
{% if len(room_members) == 1 and not order.room_confirmed %}
<a href="/manage/room/delete" role="button">Delete room</a>
{% endif %}
{% if not order.room_confirmed %} {% if not order.room_confirmed %}
{% if len(room_members) == 1 %}
<a href="/manage/room/delete" role="button">Delete room</a>
{% endif %}
{# <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 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> <a 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>
<a role="button" href="javascript:document.getElementById('modal-roomrename').setAttribute('open', 'true');">Rename room {{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}} room</a>
{% endif %} {% endif %}
{% else %} {% else %}
{% if order.room_id and not order.room_confirmed %} {% if order.room_id and not order.room_confirmed %}
@ -104,8 +108,10 @@
{% if person.status == 'pending' %} {% if person.status == 'pending' %}
<td><strong style="color:red;">UNPAID</strong></td> <td><strong style="color:red;">UNPAID</strong></td>
{% endif %} {% endif %}
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/approve/{{person.code}}">Approve</a></td> {% if order.room_owner %}
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/reject/{{person.code}}">Reject</a></td> <td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/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> </tr>
</div> </div>

View File

@ -45,7 +45,20 @@
</article> </article>
</dialog> </dialog>
<dialog id="modal-roomrename">
<article>
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
<h3>Rename this room</h3>
<p>Enter your room's new name!</p>
<p>This name will be public and shown in the room list, so nothing offensive! :)</p>
<form method="POST" action="/manage/room/rename">
<label for="name"></label>
<input type="text" name="name" required minlength="4" maxlength="64" />
<input type="submit" value="Rename room" />
</form>
</article>
</dialog>
{% endif %} {% endif %}

View File

@ -2,6 +2,9 @@
{% block title %}Furizon 2024 Nosecount{% endblock %} {% block title %}Furizon 2024 Nosecount{% endblock %}
{% block main %} {% block main %}
<main class="container"> <main class="container">
{% if order.isAdmin() %}
<script src="/res/scripts/roomManager.js"></script>
{% endif %}
<header> <header>
<picture> <picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)"> <source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
@ -11,13 +14,22 @@
<p>Welcome to the nosecount page! Here you can see all of the available rooms at the convention, as well as the occupants currently staying in each room. Use this page to find your friends and plan your meet-ups.</p> <p>Welcome to the nosecount page! Here you can see all of the available rooms at the convention, as well as the occupants currently staying in each room. Use this page to find your friends and plan your meet-ups.</p>
{% for o in orders.values() %} {% for o in orders.values() %}
{% if o.code == o.room_id and o.room_confirmed %} {% if o.code == o.room_id and o.room_confirmed %}
<h4 style="margin-top:1em;">{{o.room_name}}</h4> <h4 style="margin-top:1em;">
<span>{{o.room_name}}</span>
{% if order.isAdmin() %}
<div class="room-actions">
<a onclick="confirmAction('rename', this)" action="/manage/admin/room/rename/{{o.code}}"><img src="/res/icons/pencil.svg" class="icon" /><span>Rename</span></a>
<a onclick="confirmAction('unconfirm', this)" action="/manage/admin/room/unconfirm/{{o.code}}"><img src="/res/icons/door_open.svg" class="icon" /><span>Unconfirm</span></a>
<a class="act-del" onclick="confirmAction('delete', this)" action="/manage/admin/room/delete/{{o.code}}"><img src="/res/icons/delete.svg" class="icon" /><span>Delete</span></a>
</div>
{% endif %}
</h4>
<div class="grid people" style="padding-bottom:1em;"> <div class="grid people" style="padding-bottom:1em;">
{% for m in o.room_members %} {% for m in o.room_members %}
{% if m in orders %} {% if m in orders %}
{% with person = orders[m] %} {% with person = orders[m] %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
{% with order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %} {% with order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %} {% include 'blocks/propic.html' %}
{% endwith %} {% endwith %}
<h5>{{person.ans('fursona_name')}}</h5> <h5>{{person.ans('fursona_name')}}</h5>
@ -34,14 +46,22 @@
<h1>Unconfirmed rooms</h1> <h1>Unconfirmed rooms</h1>
<p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p> <p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p>
{% endif %} {% endif %}
<h3 style="display:inline-block">{{o.room_name}}</h3> <h3>
{% if o.room_person_no - len(o.room_members) > 0 %} <p style="display:inline-block"> - Remaining slots: {{o.room_person_no - len(o.room_members)}}</p> {% endif %} <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.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 %}
</h3>
<div class="grid people" style="padding-bottom:1em;"> <div class="grid people" style="padding-bottom:1em;">
{% for m in o.room_members %} {% for m in o.room_members %}
{% if m in orders %} {% if m in orders %}
{% with person = orders[m] %} {% with person = orders[m] %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
{% with order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %} {% with order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %} {% include 'blocks/propic.html' %}
{% endwith %} {% endwith %}
<h5>{{person.ans('fursona_name')}}</h5> <h5>{{person.ans('fursona_name')}}</h5>
@ -61,7 +81,7 @@
<div class="grid people" style="padding-bottom:1em;"> <div class="grid people" style="padding-bottom:1em;">
{% endif %} {% endif %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
{% with order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %} {% with order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %} {% include 'blocks/propic.html' %}
{% endwith %} {% endwith %}
@ -78,7 +98,7 @@
<div class="grid people" style="padding-bottom:1em;"> <div class="grid people" style="padding-bottom:1em;">
{% endif %} {% endif %}
<div style="margin-bottom: 1em;"> <div style="margin-bottom: 1em;">
{% with order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %} {% with order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = true, flag = true %}
{% include 'blocks/propic.html' %} {% include 'blocks/propic.html' %}
{% endwith %} {% endwith %}
<h5>{{person.ans('fursona_name')}}</h5> <h5>{{person.ans('fursona_name')}}</h5>
@ -86,5 +106,22 @@
{% if loop.last %}</div>{% endif %} {% if loop.last %}</div>{% endif %}
{% endfor %} {% endfor %}
<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 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="" minlength="4" maxlength="64"/>
</div>
<footer>
<input id="intentSend" type="submit" value="Confirm" />
</footer>
</article>
</dialog>
</form>
</main> </main>
{% endblock %} {% endblock %}

107
utils.py
View File

@ -1,7 +1,11 @@
from os.path import join from os.path import join
from sanic import exceptions
from config import * from config import *
import httpx import httpx
METADATA_TAG = "meta_data"
VARIATIONS_TAG = "variations"
QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.html QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.html
"number": "N", "number": "N",
"one_line_string": "S", "one_line_string": "S",
@ -32,4 +36,105 @@ async def loadQuestions():
data = res.json() data = res.json()
for q in data['results']: for q in data['results']:
TYPE_OF_QUESTIONS[q['id']] = q['type'] TYPE_OF_QUESTIONS[q['id']] = q['type']
async def loadItems():
global ITEMS_ID_MAP
global ITEM_VARIATIONS_MAP
global CATEGORIES_LIST_MAP
global ROOM_TYPE_NAMES
async with httpx.AsyncClient() as client:
p = 0
while 1:
p += 1
res = await client.get(join(base_url_event, f"items/?page={p}"), headers=headers)
if res.status_code == 404: break
data = res.json()
for q in data['results']:
# Map item id
itemName = checkAndGetName ('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 = checkAndGetName('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 = checkAndGetCategory ('item', q)
if not categoryName: continue
CATEGORIES_LIST_MAP[categoryName].append(q['id'])
if (DEV_MODE):
print (f'Mapped Items:')
print (ITEMS_ID_MAP)
print (f'Mapped Variations:')
print (ITEM_VARIATIONS_MAP)
print (f'Mapped categories:')
print (CATEGORIES_LIST_MAP)
print (f'Mapped Rooms:')
print (ROOM_TYPE_NAMES)
# Tries to get an item name from metadata. Prints a warning if an item has no metadata
def checkAndGetName(type, q):
itemName = extractMetadataName(q)
if not itemName and DEV_MODE:
print (type + ' ' + q['id'] + ' has not been mapped.')
return itemName
def checkAndGetCategory (type, q):
categoryName = extractCategory (q)
if not categoryName and DEV_MODE:
print (type + ' ' + q['id'] + ' has no category set.')
return categoryName
# Checks if the item has specified metadata name
def internalNameCheck (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 extractMetadataName (toExtract):
return extractData(toExtract, [METADATA_TAG, METADATA_NAME])
# Returns the category_name metadata from the item or None if not defined
def extractCategory (toExtract):
return extractData(toExtract, [METADATA_TAG, METADATA_CATEGORY])
def extractData (dataFrom, tags):
data = dataFrom
for t in tags:
if t not in data: return None
data = data[t]
return data
def keyFromValue(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 getOrderByCode_safe(request, code):
res = await request.app.ctx.om.get_order(code=code)
if res is None:
raise exceptions.BadRequest(f"[getOrderByCode_safe] Code {code} not found!")
return res
def getPeopleInRoomByRoomId(request, roomId):
c = request.app.ctx.om.cache
ret = []
for person in c.values():
if person.room_id == roomId:
ret.append(person)
return ret