Compare commits

...

3 Commits

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

Co-authored-by: Luca Sorace "Stranck" <stranck@users.noreply.github.com>
2024-01-08 22:02:27 +01:00
drew 01da35560f Merge pull request 'stranck-dev' (#5) from stranck-dev into drew-dev
Reviewed-on: #5
2024-01-07 20:31:00 +00:00
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 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, export_bp, stats_bp, api_bp, carpooling_bp, checkin_bp, admin_bp])
@app.exception(exceptions.SanicException)
async def clear_session(request, exception):
@ -40,8 +41,8 @@ async def clear_session(request, exception):
r = html(tpl.render(exception=exception))
if exception.status_code == 403:
del r.cookies["foxo_code"]
del r.cookies["foxo_secret"]
r.delete_cookie("foxo_code")
r.delete_cookie("foxo_secret")
return r
@app.before_server_start
@ -63,8 +64,12 @@ 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(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)
@ -157,6 +162,15 @@ async def download_ticket(request, order: Order):
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")
async def logour(request):
raise exceptions.Forbidden("You have been logged out.")

View File

@ -8,47 +8,98 @@ base_url = "http://urlllllllllllllllllllll/api/v1/"
base_url_event = f"{base_url}organizers/{ORGANIZER}/events/{EVENT_NAME}/"
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
CACHE_EXPIRE_TIME = 60 * 60 * 4
DEV_MODE = True
ITEM_IDS = {
'ticket': [126, 127, 155],
'membership_card': [128,],
'sponsorship': [55, 56], # first one = normal, second = super
'early_arrival': [133],
'late_departure': [134],
'room': 135,
# 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"
# Maps Products metadata name <--> ID
ITEMS_ID_MAP = {
'early_bird_ticket': 126,
'regular_ticket': 127,
'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,
'daily': 162,
'daily_addons': [163, 164, 165, 166] #This should be in date order. If there are holes in the daily-span, insert an unexisting id
'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.
# Map variationId -> numberOfPeopleInRoom
ROOM_MAP = {
# Map item_name -> room capacity
ROOM_CAPACITY_MAP = {
# SACRO CUORE
83: 1,
67: 2,
68: 3,
69: 4,
70: 5,
'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
75: 2
'bed_in_room_overflow1_2': 2,
}
ROOM_TYPE_NAMES = {
83: "Park Hotel Sacro Cuore (main hotel) - Single",
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"
}
# Autofilled
ROOM_TYPE_NAMES = { }
# 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'

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")
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 :)")
if not order.isAdmin(): raise exceptions.Forbidden("Birichino :)")
page = 0
orders = {}

40
ext.py
View File

@ -45,7 +45,7 @@ class Order:
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']
@ -55,32 +55,33 @@ class Order:
self.answers[i]['answer'] = "file:keep"
self.barcode = p['secret']
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.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
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 = 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']:
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'] == 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.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.fees = 0
@ -96,6 +97,8 @@ class Order:
self.payment_provider = data['payment_provider']
self.comment = data['comment']
self.phone = data['phone']
self.loadAns()
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
@ -122,8 +125,8 @@ class Order:
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
def __getitem__(self, var):
return self.data[var]
@ -136,6 +139,12 @@ class Order:
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):
async with httpx.AsyncClient() as client:
@ -147,6 +156,7 @@ class Order:
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
@ -168,7 +178,6 @@ class Order:
async with httpx.AsyncClient() as client:
res = await client.get(join(base_url_event, 'questions/'), headers=headers)
res = res.json()
for r in res['results']:
if r['identifier'] != name: continue
@ -178,6 +187,7 @@ class Order:
'answer': new_answer,
'options': r['options']
})
self.loadAns()
async def send_answers(self):
async with httpx.AsyncClient() as client:
@ -204,6 +214,7 @@ class Order:
self.pending_update = False
self.time = -1
self.loadAns()
@dataclass
class Quotas:
@ -254,6 +265,7 @@ class OrderManager:
self.order_list.remove(code)
async def fill_cache(self):
await loadItems()
await loadQuestions()
self.empty()
p = 0

View File

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

View File

@ -6,14 +6,19 @@ 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 ""
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:
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)
if(sendAnswer):
await order.send_answers()
@ -41,17 +46,30 @@ 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 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:
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
# 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
if aspect_ratio > 1:
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.convert('RGB')
width, height = img.size
img.thumbnail((512,512))
imgBytes = BytesIO()
img.save(imgBytes, format='jpeg')
imgBytes = imgBytes.getvalue()
with open(f"res/propic/{fn}_{order.code}_{h}.jpg", "wb") as f:
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)
except Exception:
import traceback
print(traceback.format_exc())
raise exceptions.BadRequest("The image you uploaded is not valid.")
if DEV_MODE: 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")

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 */
@import url('propic.css');
@import url('admin.css');
@import url('room.css');
summary:has(span.status) {
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:
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.")
if order.ans('room_confirmed'):
@ -177,6 +177,9 @@ 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")
@ -231,6 +234,9 @@ 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")
@ -253,6 +259,32 @@ async def reject_roomreq(request, code, order: Order):
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")
async def confirm_room(request, order: Order, quotas: Quotas):
if not order:

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">
<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>
.people div {text-align:center;}
.people h3, .people p, .people h5 {margin:0;}
@ -99,6 +100,7 @@
{% if order %}
<a href="/manage/carpooling">Carpooling</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>
{% endif %}
<br clear="both" />

View File

@ -1,5 +1,5 @@
<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 %}
@ -11,23 +11,23 @@
<form method="POST" enctype="multipart/form-data" action="/manage/propic/upload">
<div class="grid" style="text-align:center;margin-bottom:1em;">
<div>
{% if not order.ans('propic') %}
<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 %}
{% with 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" />
{% endif %}
</div>
{% if order.is_fursuiter %}
<div>
{% if not order.ans('propic_fursuiter') %}
<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 %}
{% with 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" />
{% endif %}
</div>
{% endif %}
</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>
{% 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 %}

View File

@ -1,5 +1,5 @@
<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>
<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>
@ -77,13 +77,17 @@
<p class="grid">
{% if order.room_owner %}
{% if len(room_members) == 1 and 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 %}
{% 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 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 %}
{% else %}
{% if order.room_id and not order.room_confirmed %}
@ -104,8 +108,10 @@
{% if person.status == 'pending' %}
<td><strong style="color:red;">UNPAID</strong></td>
{% endif %}
{% if order.room_owner %}
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/approve/{{person.code}}">Approve</a></td>
<td style="width:1%;white-space: nowrap;"><a role="button" href="/manage/room/reject/{{person.code}}">Reject</a></td>
{% endif %}
</tr>
</div>

View File

@ -45,7 +45,20 @@
</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" />
<input type="submit" value="Rename room" />
</form>
</article>
</dialog>
{% endif %}

View File

@ -2,6 +2,9 @@
{% block title %}Furizon 2024 Nosecount{% endblock %}
{% block main %}
<main class="container">
{% if order.isAdmin() %}
<script src="/res/scripts/roomManager.js"></script>
{% endif %}
<header>
<picture>
<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>
{% for o in orders.values() %}
{% 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;">
{% for m in o.room_members %}
{% if m in orders %}
{% with person = orders[m] %}
<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' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
@ -34,14 +46,22 @@
<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>
{% endif %}
<h3 style="display:inline-block">{{o.room_name}}</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 %}
<h3>
<span>{{o.room_name}}</span>
{% if o.room_person_no - len(o.room_members) > 0 %} <span class="nsc-room-counter"> - Remaining slots: {{o.room_person_no - len(o.room_members)}}</span> {% endif %}
{% if order.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;">
{% for m in o.room_members %}
{% if m in orders %}
{% with person = orders[m] %}
<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' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
@ -61,7 +81,7 @@
<div class="grid people" style="padding-bottom:1em;">
{% endif %}
<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' %}
{% endwith %}
@ -78,7 +98,7 @@
<div class="grid people" style="padding-bottom:1em;">
{% endif %}
<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' %}
{% endwith %}
<h5>{{person.ans('fursona_name')}}</h5>
@ -86,5 +106,22 @@
{% if loop.last %}</div>{% endif %}
{% 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>
{% endblock %}

105
utils.py
View File

@ -1,7 +1,11 @@
from os.path import join
from sanic import exceptions
from config import *
import httpx
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",
@ -33,3 +37,104 @@ async def loadQuestions():
data = res.json()
for q in data['results']:
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