Admin panel
+Data
Clear cache + Export CSV + Export hotel CSV ++
Rooms
Manage rooms Verify Rooms - Remind badge upload + Fill Rooms+
Profiles
+ Remind badge upload + Auto-confirm Rooms ++ {% include 'components/confirm_action_modal.html' %}
diff --git a/.gitignore b/.gitignore
index 23d733b..90a11f9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -168,4 +168,5 @@ diomerdas
furizon.net/site/*
furizon.net.zip
stuff/secrets.py
-backups/*
\ No newline at end of file
+backups/*
+log.txt
diff --git a/admin.py b/admin.py
index 9c37864..315316e 100644
--- a/admin.py
+++ b/admin.py
@@ -1,10 +1,16 @@
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")
@@ -42,6 +48,7 @@ async def login_as(request, code, order:Order):
@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())
@@ -54,8 +61,20 @@ async def unconfirm_room(request, code, order:Order):
await unconfirm_room_by_order(order=dOrder, throw=True, request=request)
return redirect(f'/manage/nosecount')
+@bp.get('/room/autoconfirm')
+async def autoconfirm_room(request, order:Order):
+ await clear_cache(request, order)
+ orders = request.app.ctx.om.cache.values()
+ for order in orders:
+ if(order.code == order.room_id and not order.room_confirmed and len(order.room_members) == order.room_person_no):
+ logger.info(f"Auto-Confirming room {order.room_id}")
+ await confirm_room_by_order(order, request)
+ await clear_cache(request, order)
+ return redirect(f'/manage/admin')
+
@bp.get('/room/delete/')
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)
@@ -75,6 +94,7 @@ async def delete_room(request, code, order:Order):
@bp.post('/room/rename/
')
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')
@@ -85,6 +105,192 @@ async def rename_room(request, code, order:Order):
await dOrder.send_answers()
return redirect(f'/manage/nosecount')
+@bp.get('/room/wizard')
+async def room_wizard(request, order:Order):
+ '''Tries to autofill unconfirmed rooms and other matches together'''
+ # Clear cache first
+ await clear_cache(request, order)
+
+ #Separate orders which have incomplete rooms and which have no rooms
+ all_orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: (x[1].room_person_no, len(x[1].room_members)), reverse=True) if (value.status not in ['canceled', 'expired'] and not value.daily and value.bed_in_room != ITEM_VARIATIONS_MAP["bed_in_room"]["bed_in_room_no_room"])}
+ orders = {key:value for key,value in sorted(all_orders.items(), key=lambda x: x[1].ans('fursona_name')) if not value.room_confirmed}
+ # Orders with incomplete rooms
+ incomplete_orders = {key:value for key,value in orders.items() if value.code == value.room_id and (value.room_person_no - len(value.room_members)) > 0}
+ # Roomless furs
+ roomless_orders = {key:value for key,value in orders.items() if(not value.room_id and not value.daily and value.bed_in_room != ITEM_VARIATIONS_MAP["bed_in_room"]["bed_in_room_no_room"])}
+
+ # Result map
+ result_map = {}
+
+ # Check overflows
+ room_quota_overflow = {}
+ for key, value in ITEM_VARIATIONS_MAP['bed_in_room'].items():
+ if key != "bed_in_room_no_room":
+ room_quota = get_quota(ITEMS_ID_MAP['bed_in_room'], value)
+ capacity = ROOM_CAPACITY_MAP[key] if key in ROOM_CAPACITY_MAP else 1
+ current_quota = len(list(filter(lambda y: y.bed_in_room == value and y.room_owner == True, all_orders.values())))
+ room_quota_overflow[value] = current_quota - int(room_quota.size / capacity) if room_quota else 0
+ if DEV_MODE and EXTRA_PRINTS:
+ print(f"There are {current_quota} of room type {key} out of a total of ({room_quota.size} / {capacity})")
+
+ # Init rooms to remove
+ result_map["void"] = []
+
+ # Remove rooms that are over quota
+ for room_type, overflow_qty in {key:value for key,value in room_quota_overflow.items() if value > 0}.items():
+ sorted_rooms = sorted(incomplete_orders.values(), key=lambda r: len(r.room_members))
+ sorted_rooms = [r for r in sorted_rooms if r.bed_in_room == room_type]
+ for room_to_remove in sorted_rooms[:overflow_qty]:
+ # Room codes to remove
+ result_map["void"].append(room_to_remove.code)
+ # Move room members to the roomless list
+ for member_code in room_to_remove.room_members:
+ roomless_orders[member_code] = all_orders[member_code]
+ del incomplete_orders[room_to_remove.code]
+
+ # Fill already existing rooms
+ for room_order in incomplete_orders.items():
+ room = room_order[1]
+ to_add = []
+ count = room.room_person_no
+ alreadyPresent = len(room.room_members)
+ missing_slots = count - alreadyPresent
+ for _ in range(missing_slots):
+ compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room}
+ if len(compatible_roomates.items()) == 0: break
+ # Try picking a roomate that's from the same country and room type
+ country = room.country.lower()
+ roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country}
+ if len(roomless_by_country.items()) > 0:
+ code_to_add = list(roomless_by_country.keys())[0]
+ to_add.append(code_to_add)
+ del roomless_orders[code_to_add]
+ else:
+ # If not, add first roomless there is
+ code_to_add = list(compatible_roomates.keys())[0]
+ to_add.append(code_to_add)
+ del roomless_orders[code_to_add]
+ result_map[room.code] = {
+ 'type': 'add_existing',
+ 'to_add': to_add,
+ 'count': count,
+ 'previouslyPresent': alreadyPresent
+ }
+
+ generated_counter = 0
+ # Create additional rooms
+ while len(roomless_orders.items()) > 0:
+ room = list(roomless_orders.items())[0][1]
+ to_add = []
+ count = room.room_person_no
+ alreadyPresent = len(room.room_members)
+ missing_slots = count - alreadyPresent
+ for _ in range(missing_slots):
+ compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room}
+ if len(compatible_roomates.items()) == 0: break
+ # Try picking a roomate that's from the same country and room type
+ country = room.country.lower()
+ roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country}
+ if len(roomless_by_country.items()) > 0:
+ code_to_add = list(roomless_by_country.keys())[0]
+ to_add.append(code_to_add)
+ del roomless_orders[code_to_add]
+ else:
+ # If not, add first roomless there is
+ code_to_add = list(compatible_roomates.keys())[0]
+ to_add.append(code_to_add)
+ del roomless_orders[code_to_add]
+ generated_counter += 1
+ result_map[room.code] = {
+ 'type': 'new',
+ 'room_name': f'Generated Room {generated_counter}',
+ 'room_type': room.bed_in_room,
+ 'to_add': to_add,
+ 'count': count,
+ 'previouslyPresent': alreadyPresent
+ }
+
+ result_map["infinite"] = { 'to_add': [] }
+ result_map = {k: v for k, v in sorted(result_map.items(), key=lambda x: ((x[1]["count"], x[1]["previouslyPresent"]) if("count" in x[1] and "previouslyPresent" in x[1]) else (4316, 0) ))}
+ tpl = request.app.ctx.tpl.get_template('wizard.html')
+ return html(tpl.render(order=order, all_orders=all_orders, unconfirmed_orders=orders, data=result_map, jsondata=json.dumps(result_map, skipkeys=True, ensure_ascii=False)))
+
+@bp.post('/room/wizard/submit')
+async def submit_from_room_wizard(request:Request, order:Order):
+ '''Will apply changes to the rooms'''
+ await clear_cache(request, order)
+
+ data = json.loads(request.body)
+
+ # Phase 1 - Delete all rooms in void
+ if 'void' in data:
+ for room_code in data['void']:
+ ppl = await get_people_in_room_by_code(request, room_code)
+ for p in ppl:
+ await p.edit_answer('room_id', None)
+ await p.edit_answer('room_confirmed', "False")
+ await p.edit_answer('room_name', None)
+ await p.edit_answer('pending_room', None)
+ await p.edit_answer('pending_roommates', None)
+ await p.edit_answer('room_members', None)
+ await p.edit_answer('room_owner', None)
+ await p.edit_answer('room_secret', None)
+ await p.send_answers()
+ logger.info(f"Deleted rooms {', '.join(data['void'])}")
+
+ # Phase 2 - Join roomless to other rooms or add new rooms
+ for room_code, value in {key:value for key,value in data.items() if key.lower() not in ['void', 'infinite']}.items():
+ if not value['to_add'] or len(value['to_add']) == 0: continue
+ room_order = await request.app.ctx.om.get_order(code=room_code)
+ # Preconditions
+ if not room_order: raise exceptions.BadRequest(f"Order {room_code} does not exist.")
+ if room_order.daily == True: raise exceptions.BadRequest(f"Order {room_code} is daily.")
+ if room_order.status != 'paid': raise exceptions.BadRequest(f"Order {room_code} hasn't paid.")
+ if room_order.room_owner:
+ if room_order.room_person_no < len(room_order.room_members) + (len(value['to_add']) if value['to_add'] else 0):
+ raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.")
+ elif room_order.room_person_no < (len(value['to_add']) if value['to_add'] else 0):
+ raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.")
+
+ # Adding roomless orders to existing rooms
+ if value['type'] == 'add_existing' or value['type'] == 'new':
+ if value['type'] == 'new':
+ if room_order.room_owner: exceptions.BadRequest(f"Order {room_code} is already a room owner.")
+ # Create room data
+ await room_order.edit_answer('room_name', value['room_name'])
+ await room_order.edit_answer('room_id', room_order.code)
+ await room_order.edit_answer('room_secret', ''.join(choice('0123456789') for _ in range(6)))
+ elif not room_order.room_owner:
+ raise exceptions.BadRequest(f"Order {room_code} is not a room owner.")
+ # Add members
+ for new_member_code in value['to_add']:
+ pending_member = await request.app.ctx.om.get_order(code=new_member_code)
+ # Preconditions
+ if pending_member.daily == True: raise exceptions.BadRequest(f"Order {pending_member.code} is daily.")
+ #if pending_member.status != 'paid': raise exceptions.BadRequest(f"Order {new_member_code} hasn't paid.") # Since we don't confirm rooms anymore, we don't need to check if they're paid or not
+ if pending_member.bed_in_room != room_order.bed_in_room: raise exceptions.BadRequest(f"Order {new_member_code} has a different room type than {room_code}.")
+ if pending_member.room_owner: exceptions.BadRequest(f"Order {new_member_code} is already a room owner.")
+ if pending_member.room_id and pending_member.room_id not in data['void']: exceptions.BadRequest(f"Order {new_member_code} is in another room.")
+ await pending_member.edit_answer('room_id', room_order.code)
+ await pending_member.edit_answer('room_confirmed', "True")
+ await pending_member.edit_answer('pending_room', None)
+ await pending_member.send_answers()
+ logger.info(f"{'Created' if value['type'] == 'new' else 'Edited'} {str(room_order)}")
+ # Confirm members that were already inside the room
+ if value['type'] == 'add_existing':
+ for already_member in list(filter(lambda rm: rm.code in room_order.room_members and rm.code != room_order.code, request.app.ctx.om.cache.values())):
+ await already_member.edit_answer('room_confirmed', "True")
+ await already_member.send_answers()
+ else: raise exceptions.BadRequest(f"Unexpected type ({value['type']})")
+ await room_order.edit_answer('pending_room', None)
+ await room_order.edit_answer('pending_roommates', None)
+ # await room_order.edit_answer('room_confirmed', "True") Use the autoconfirm button in the admin panel
+ await room_order.edit_answer('room_members', ','.join(list(set([*room_order.room_members, room_order.code, *value['to_add']]))))
+ await room_order.send_answers()
+ await clear_cache(request, order)
+ return text('done', status=200)
+
+
@bp.get('/propic/remind')
async def propic_remind_missing(request, order:Order):
await clear_cache(request, order)
@@ -98,4 +304,81 @@ async def propic_remind_missing(request, order:Order):
# print(f"{order.code}: prp={missingPropic} fpr={missingFursuitPropic} - {order.name}")
await send_missing_propic_message(order, missingPropic, missingFursuitPropic)
- return redirect(f'/manage/admin')
\ No newline at end of file
+ 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"})
\ No newline at end of file
diff --git a/api.py b/api.py
index 2e13108..c6e197a 100644
--- a/api.py
+++ b/api.py
@@ -9,7 +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")
@@ -32,7 +34,8 @@ async def api_members(request):
'propic_fursuiter': o.ans('propic_fursuiter'),
'staff_role': o.ans('staff_role'),
'country': o.country,
- 'is_checked_in': False,
+ 'room_id': o.room_id,
+ 'is_checked_in': o.checked_in,
'points': random.randint(0,50) if random.random() > 0.3 else 0
})
@@ -110,6 +113,10 @@ async def token_test(request):
return response.json({'ok': False, 'error': 'The token you have provided is not correct.'}, status=401)
return response.json({'ok': True, 'message': 'This token is valid :)'})
+
+@bp.get("/ping")
+async def ping(request):
+ return response.text("pong")
@bp.get("/welcome")
async def welcome_app(request):
@@ -137,15 +144,18 @@ async def welcome_app(request):
'propic_fursuiter': o.ans('propic_fursuiter'),
'staff_role': o.ans('staff_role'),
'country': o.country,
- 'is_checked_in': False,
+ 'is_checked_in': o.checked_in,
'points': random.randint(0,50) if random.random() > 0.3 else 0,
'can_scan_nfc': o.can_scan_nfc,
+ 'room_id': o.room_id,
+ #'mail': o.email,
'actual_room_id': o.actual_room,
**ret
})
@bp.get("/scan/
We had to unconfirm your room '{1}' due to the following issues:
Please contact your room's owner or contact our support for further informations at https://furizon.net/contact/.
Thank you.
Manage booking",
+ 'html': "Hello {0}
We had to unconfirm or change your room '{1}' due to the following issues:
Please contact your room's owner or contact our support for further informations at https://furizon.net/contact/. Data Rooms Profiles
Thank you.
Manage booking",
- 'plain': "Hello {0}\nWe had to unconfirm 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"
+ '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"
},
diff --git a/metrics.py b/metrics.py
index 43f037d..8f5da5f 100644
--- a/metrics.py
+++ b/metrics.py
@@ -1,6 +1,7 @@
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
@@ -47,7 +48,7 @@ def getMetricsText():
def getRoomCountersText(request):
out = []
- try :
+ try:
daily = 0
counters = {}
counters_early = {}
@@ -61,11 +62,13 @@ def getRoomCountersText(request):
if(order.daily):
daily += 1
else:
- 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
+ # 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}')
@@ -76,7 +79,8 @@ def getRoomCountersText(request):
out.append(f'webint_order_room_counter{{label="Daily"}} {daily}')
except Exception as e:
- print(e)
+ print(traceback.format_exc())
+
logger.warning("Error in loading metrics rooms")
return "\n".join(out)
diff --git a/propic.py b/propic.py
index 3c4138a..d052054 100644
--- a/propic.py
+++ b/propic.py
@@ -6,6 +6,7 @@ from PIL import Image
from io import BytesIO
from hashlib import sha224
from time import time
+from utils import isSessionAdmin
import os
bp = Blueprint("propic", url_prefix="/manage/propic")
@@ -38,7 +39,7 @@ async def upload_propic(request, order: Order):
if order.propic_locked:
raise exceptions.BadRequest("You have been limited from further editing the propic.")
- if request.form.get('submit') != 'Upload' and time() > PROPIC_DEADLINE:
+ if request.form.get('submit') != 'Upload' and (time() > PROPIC_DEADLINE and not await isSessionAdmin(request, order)):
raise exceptions.BadRequest("The deadline has passed. You cannot modify the badges at this moment.")
if request.form.get('submit') == 'Delete main image':
diff --git a/res/botbg2.png b/res/botbg2.png
deleted file mode 100644
index 632657c..0000000
Binary files a/res/botbg2.png and /dev/null differ
diff --git a/res/error_openbox.png b/res/error_openbox.png
deleted file mode 100644
index b54a081..0000000
Binary files a/res/error_openbox.png and /dev/null differ
diff --git a/res/icons/book-plus.svg b/res/icons/book-plus.svg
new file mode 100644
index 0000000..7d014d2
--- /dev/null
+++ b/res/icons/book-plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/res/scripts/adminManager.js b/res/scripts/adminManager.js
new file mode 100644
index 0000000..f6584de
--- /dev/null
+++ b/res/scripts/adminManager.js
@@ -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');
+}
\ No newline at end of file
diff --git a/res/scripts/roomManager.js b/res/scripts/roomManager.js
index 29537bf..a2ff674 100644
--- a/res/scripts/roomManager.js
+++ b/res/scripts/roomManager.js
@@ -1,11 +1,11 @@
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 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("#intentSend")
+ let intentSend = document.querySelector("#modalOrderEditDialog #intentSend")
// Resetting ui
intentEdit.removeAttribute('required')
intentEdit.removeAttribute('minlength')
@@ -27,5 +27,5 @@ function confirmAction (intent, sender) {
case 'delete':
break
}
- document.getElementById('modalRoomconfirm').setAttribute('open', 'true');
+ document.getElementById('modalOrderEditDialog').setAttribute('open', 'true');
}
\ No newline at end of file
diff --git a/res/scripts/wizardManager.js b/res/scripts/wizardManager.js
new file mode 100644
index 0000000..af63ea3
--- /dev/null
+++ b/res/scripts/wizardManager.js
@@ -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 ();
\ No newline at end of file
diff --git a/res/styles/admin.css b/res/styles/admin.css
index d1b0f9b..5385e04 100644
--- a/res/styles/admin.css
+++ b/res/styles/admin.css
@@ -3,6 +3,18 @@ div.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;
@@ -16,4 +28,26 @@ div.room-actions > a:hover {
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;
}
\ No newline at end of file
diff --git a/res/styles/base.css b/res/styles/base.css
index 37d9a1a..51bb7d9 100644
--- a/res/styles/base.css
+++ b/res/styles/base.css
@@ -27,6 +27,14 @@ summary:has(span.status) {
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);}
diff --git a/res/styles/navbar.css b/res/styles/navbar.css
index 4d34f22..43c99cd 100644
--- a/res/styles/navbar.css
+++ b/res/styles/navbar.css
@@ -14,7 +14,8 @@ nav#topbar {
top: 0rem;
transition: top 300ms;
line-height: 2em;
- max-width:98vw;
+ max-width:100%;
+ overflow-x: hidden;
}
nav#topbar a {
diff --git a/res/styles/wizard.css b/res/styles/wizard.css
new file mode 100644
index 0000000..d11a5ad
--- /dev/null
+++ b/res/styles/wizard.css
@@ -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;
+ }
+
+}
\ No newline at end of file
diff --git a/room.py b/room.py
index 3e1382f..2fa7024 100644
--- a/room.py
+++ b/room.py
@@ -5,9 +5,19 @@ 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
+from time import time
bp = Blueprint("room", url_prefix="/manage/room")
+@bp.middleware
+async def deadline_check(request: Request):
+ order = await get_order(request)
+ if not order:
+ raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
+ if time() > ROOM_DEADLINE and not await isSessionAdmin(request, order):
+ raise exceptions.BadRequest("The deadline has passed. You cannot modify the room at this moment.")
+
@bp.post("/create")
async def room_create_post(request, order: Order):
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
@@ -303,40 +313,7 @@ async def confirm_room(request, order: Order, quotas: Quotas):
#if quotas.get_left(len(order.room_members)) == 0:
# raise exceptions.BadRequest("There are no more rooms of this size to reserve.")
- 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 and order.room_person_no != None:
- 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()
+ await confirm_room_by_order(order, request)
return redirect('/manage/welcome')
diff --git a/stuff/testAsyncio.py b/stuff/testAsyncio.py
new file mode 100644
index 0000000..01e0ec9
--- /dev/null
+++ b/stuff/testAsyncio.py
@@ -0,0 +1,11 @@
+# python merda
+import asyncio
+
+async def a():
+ print("a")
+
+def b():
+ loop = asyncio.get_event_loop()
+ print(loop)
+
+b()
\ No newline at end of file
diff --git a/tpl/admin.html b/tpl/admin.html
index 2c1b0fb..409ab19 100644
--- a/tpl/admin.html
+++ b/tpl/admin.html
@@ -2,6 +2,7 @@
{% block title %}Admin panel{% endblock %}
{% block main %}
Admin panel
+
+
+
+ {% include 'components/confirm_action_modal.html' %}
⚠️ The deadline to upload pictures for the badge has expired. For last-minute changes, please contact the support over at info@furizon.net. 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.
{% else %}
@@ -43,9 +43,9 @@
{% endif %}
Check here for any fur who share your room type. ⚠️ The deadline to edit your room has passed. If your room is not full it will be subject to changes by the staff as we optimize for hotel capacity. ⚠️ 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. ⚠️ Your room hasn't been confirmed yet. Unconfirmed rooms are subject to changes by the staff as we optimize for hotel capacity. ⚠️ 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. ⚠️ Your room hasn't been confirmed yet. Unconfirmed rooms are subject to changes by the staff as we optimize for hotel capacity. ✅ Your {{[None,'single','double','triple','quadruple','quintuple'][len(room_members)]}} room has been confirmed UNPAID You have have asked to join the room of another member. Wait for them to confirm or reject your request. 🎲 If you don't join a room or create your one within the room deadline, we will randomly put you into a room with free spots. To join a room, ask somebody to send you their room code.
- Create a room
- Join a room
+ ROOM_DEADLINE and not isSessionAdmin else ''}}>Create a room
+ ROOM_DEADLINE and not isSessionAdmin else ''}}>Join a room
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. 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 in the dedicated page. 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 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.Invite
@@ -63,13 +70,13 @@
UNPAID
{% endif %}
{% if order.room_owner %}
- Approve
- Reject
+ ROOM_DEADLINE and not isSessionAdmin else ''}}>Approve
+ ROOM_DEADLINE and not isSessionAdmin else ''}}>Reject
{% endif %}
Shuttle
- Review rooms ?
+
Once finished, scroll down to either 'Confirm' changes or 'Undo' them.
+ {% for room in data.items() %}
+ {% if room[0] in all_orders %}
+ {%with room_order = unconfirmed_orders[room[0]] %}
+
+ {{room_order.room_name if room_order.room_name else room[1]['room_name'] if room[1] and room[1]['room_name'] else ''}} - {{room_order.room_person_no}} People max
+
+ {{person.ans('fursona_name')}}
+ {{person.ans('fursona_name')}}
+ Empty room ?
+