stranck-dev #31
176
admin.py
176
admin.py
|
@ -1,5 +1,6 @@
|
|||
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 *
|
||||
|
@ -8,6 +9,8 @@ from io import StringIO
|
|||
from sanic.log import logger
|
||||
import csv
|
||||
import time
|
||||
import json
|
||||
import math
|
||||
|
||||
bp = Blueprint("admin", url_prefix="/manage/admin")
|
||||
|
||||
|
@ -101,6 +104,179 @@ 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: len(x[1].room_members), reverse=True) if value.status not in ['c', 'e'] and not value.daily}
|
||||
orders = {key:value for key,value in sorted(all_orders.items(), key=lambda x: x[1].ans('fursona_name')) if not value.room_confirmed}
|
||||
# Orders with incomplete rooms
|
||||
incomplete_orders = {key:value for key,value in orders.items() if value.code == value.room_id and (value.room_person_no - len(value.room_members)) > 0}
|
||||
# Roomless furs
|
||||
roomless_orders = {key:value for key,value in orders.items() if not value.room_id and not value.daily}
|
||||
|
||||
# Result map
|
||||
result_map = {}
|
||||
|
||||
# Check overflows
|
||||
room_quota_overflow = {}
|
||||
for key, value in ITEM_VARIATIONS_MAP['bed_in_room'].items():
|
||||
room_quota = get_quota(ITEMS_ID_MAP['bed_in_room'], value)
|
||||
capacity = ROOM_CAPACITY_MAP[key] if key in ROOM_CAPACITY_MAP else 1
|
||||
current_quota = len(list(filter(lambda y: y.bed_in_room == value and y.room_owner == True, orders.values())))
|
||||
room_quota_overflow[value] = current_quota - int(room_quota.size / capacity) if room_quota else 0
|
||||
|
||||
# Init rooms to remove
|
||||
result_map["void"] = []
|
||||
|
||||
# Remove rooms that are over quota
|
||||
for room_type, overflow_qty in {key:value for key,value in room_quota_overflow.items() if value > 0}.items():
|
||||
sorted_rooms = sorted(incomplete_orders.values(), key=lambda r: len(r.room_members))
|
||||
for room_to_remove in sorted_rooms[:overflow_qty]:
|
||||
# Room codes to remove
|
||||
result_map["void"].append(room_to_remove.code)
|
||||
# Move room members to the roomless list
|
||||
for member_code in room_to_remove.room_members:
|
||||
roomless_orders[member_code] = all_orders[member_code]
|
||||
del incomplete_orders[room_to_remove.code]
|
||||
|
||||
# Fill already existing rooms
|
||||
for room_order in incomplete_orders.items():
|
||||
room = room_order[1]
|
||||
to_add = []
|
||||
missing_slots = room.room_person_no - len(room.room_members)
|
||||
for i in range(missing_slots):
|
||||
compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room}
|
||||
if len(compatible_roomates.items()) == 0: break
|
||||
# Try picking a roomate that's from the same country and room type
|
||||
country = room.country.lower()
|
||||
roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country}
|
||||
if len(roomless_by_country.items()) > 0:
|
||||
code_to_add = list(roomless_by_country.keys())[0]
|
||||
to_add.append(code_to_add)
|
||||
del roomless_orders[code_to_add]
|
||||
else:
|
||||
# If not, add first roomless there is
|
||||
code_to_add = list(compatible_roomates.keys())[0]
|
||||
to_add.append(code_to_add)
|
||||
del roomless_orders[code_to_add]
|
||||
result_map[room.code] = {
|
||||
'type': 'add_existing',
|
||||
'to_add': to_add
|
||||
}
|
||||
|
||||
generated_counter = 0
|
||||
# Create additional rooms
|
||||
while len(roomless_orders.items()) > 0:
|
||||
room = list(roomless_orders.items())[0][1]
|
||||
to_add = []
|
||||
missing_slots = room.room_person_no - len(room.room_members)
|
||||
for i in range(missing_slots):
|
||||
compatible_roomates = {key:value for key,value in roomless_orders.items() if value.bed_in_room == room.bed_in_room}
|
||||
if len(compatible_roomates.items()) == 0: break
|
||||
# Try picking a roomate that's from the same country and room type
|
||||
country = room.country.lower()
|
||||
roomless_by_country = {key:value for key,value in compatible_roomates.items() if value.country.lower() == country}
|
||||
if len(roomless_by_country.items()) > 0:
|
||||
code_to_add = list(roomless_by_country.keys())[0]
|
||||
to_add.append(code_to_add)
|
||||
del roomless_orders[code_to_add]
|
||||
else:
|
||||
# If not, add first roomless there is
|
||||
code_to_add = list(compatible_roomates.keys())[0]
|
||||
to_add.append(code_to_add)
|
||||
del roomless_orders[code_to_add]
|
||||
generated_counter += 1
|
||||
result_map[room.code] = {
|
||||
'type': 'new',
|
||||
'room_name': f'Generated Room {generated_counter}',
|
||||
'room_type': room.bed_in_room,
|
||||
'to_add': to_add
|
||||
}
|
||||
|
||||
result_map["infinite"] = { 'to_add': [] }
|
||||
tpl = request.app.ctx.tpl.get_template('wizard.html')
|
||||
return html(tpl.render(order=order, all_orders=all_orders, unconfirmed_orders=orders, data=result_map, jsondata=json.dumps(result_map, skipkeys=True, ensure_ascii=False)))
|
||||
|
||||
@bp.post('/room/wizard/submit')
|
||||
async def submit_from_room_wizard(request:Request, order:Order):
|
||||
'''Will apply changes to the rooms'''
|
||||
await request.app.ctx.om.fill_cache()
|
||||
|
||||
data = json.loads(request.body)
|
||||
|
||||
# Phase 1 - Delete all rooms in void
|
||||
if 'void' in data:
|
||||
for room_code in data['void']:
|
||||
ppl = await get_people_in_room_by_code(request, room_code)
|
||||
for p in ppl:
|
||||
await p.edit_answer('room_id', None)
|
||||
await p.edit_answer('room_confirmed', "False")
|
||||
await p.edit_answer('room_name', None)
|
||||
await p.edit_answer('pending_room', None)
|
||||
await p.edit_answer('pending_roommates', None)
|
||||
await p.edit_answer('room_members', None)
|
||||
await p.edit_answer('room_owner', None)
|
||||
await p.edit_answer('room_secret', None)
|
||||
await p.send_answers()
|
||||
logger.info(f"Deleted rooms {', '.join(data['void'])}")
|
||||
|
||||
# Phase 2 - Join roomless to other rooms or add new rooms
|
||||
for room_code, value in {key:value for key,value in data.items() if key.lower() not in ['void', 'infinite']}.items():
|
||||
if not value['to_add'] or len(value['to_add']) == 0: continue
|
||||
room_order = await request.app.ctx.om.get_order(code=room_code)
|
||||
# Preconditions
|
||||
if not room_order: raise exceptions.BadRequest(f"Order {room_code} does not exist.")
|
||||
if room_order.daily == True: raise exceptions.BadRequest(f"Order {room_code} is daily.")
|
||||
if room_order.status != 'paid': raise exceptions.BadRequest(f"Order {room_code} hasn't paid.")
|
||||
if room_order.room_owner:
|
||||
if room_order.room_person_no < len(room_order.room_members) + (len(value['to_add']) if value['to_add'] else 0):
|
||||
raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.")
|
||||
elif room_order.room_person_no < (len(value['to_add']) if value['to_add'] else 0):
|
||||
raise exceptions.BadRequest(f"Input exceeds room {room_order.code} capacity.")
|
||||
|
||||
# Adding roomless orders to existing rooms
|
||||
if value['type'] == 'add_existing' or value['type'] == 'new':
|
||||
if value['type'] == 'new':
|
||||
if room_order.room_owner: exceptions.BadRequest(f"Order {room_code} is already a room owner.")
|
||||
# Create room data
|
||||
await room_order.edit_answer('room_name', value['room_name'])
|
||||
await room_order.edit_answer('room_id', room_order.code)
|
||||
await room_order.edit_answer('room_secret', ''.join(choice('0123456789') for _ in range(6)))
|
||||
elif not room_order.room_owner:
|
||||
raise exceptions.BadRequest(f"Order {room_code} is not a room owner.")
|
||||
# Add members
|
||||
for new_member_code in value['to_add']:
|
||||
pending_member = await request.app.ctx.om.get_order(code=new_member_code)
|
||||
# Preconditions
|
||||
if pending_member.daily == True: raise exceptions.BadRequest(f"Order {pending_member.code} is daily.")
|
||||
if pending_member.status != 'paid': raise exceptions.BadRequest(f"Order {new_member_code} hasn't paid.")
|
||||
if pending_member.bed_in_room != room_order.bed_in_room: raise exceptions.BadRequest(f"Order {new_member_code} has a different room type than {room_code}.")
|
||||
if pending_member.room_owner: exceptions.BadRequest(f"Order {new_member_code} is already a room owner.")
|
||||
if pending_member.room_id and pending_member.room_id not in data['void']: exceptions.BadRequest(f"Order {new_member_code} is in another room.")
|
||||
await pending_member.edit_answer('room_id', room_order.code)
|
||||
await pending_member.edit_answer('room_confirmed', "True")
|
||||
await pending_member.edit_answer('pending_room', None)
|
||||
await pending_member.send_answers()
|
||||
logger.info(f"{'Created' if value['type'] == 'new' else 'Edited'} {str(room_order)}")
|
||||
# Confirm members that were already inside the room
|
||||
if value['type'] == 'add_existing':
|
||||
for already_member in list(filter(lambda rm: rm.code in room_order.room_members and rm.code != room_order.code, request.app.ctx.om.cache.values())):
|
||||
await already_member.edit_answer('room_confirmed', "True")
|
||||
await already_member.send_answers()
|
||||
else: raise exceptions.BadRequest(f"Unexpected type ({value['type']})")
|
||||
await room_order.edit_answer('pending_room', None)
|
||||
await room_order.edit_answer('pending_roommates', None)
|
||||
await room_order.edit_answer('room_confirmed', "True")
|
||||
await room_order.edit_answer('room_members', ','.join(list(set([*room_order.room_members, room_order.code, *value['to_add']]))))
|
||||
await room_order.send_answers()
|
||||
await request.app.ctx.om.fill_cache()
|
||||
return text('done', status=200)
|
||||
|
||||
|
||||
@bp.get('/propic/remind')
|
||||
async def propic_remind_missing(request, order:Order):
|
||||
await clear_cache(request, order)
|
||||
|
|
7
app.py
7
app.py
|
@ -51,7 +51,7 @@ async def handleException(request, exception):
|
|||
statusCode = exception.status_code if hasattr(exception, 'status_code') else 500
|
||||
try:
|
||||
tpl = app.ctx.tpl.get_template('error.html')
|
||||
r = html(tpl.render(exception=exception, status_code=statusCode))
|
||||
r = html(tpl.render(exception=exception, status_code=statusCode), status=statusCode)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
@ -101,6 +101,11 @@ async def gen_barcode(request, code):
|
|||
|
||||
return raw(img.getvalue(), content_type="image/png")
|
||||
|
||||
@app.route("/manage/lol")
|
||||
async def lol(request: Request):
|
||||
await get_quotas(request)
|
||||
return text('hi')
|
||||
|
||||
@app.route(f"/{ORGANIZER}/{EVENT_NAME}/order/<code>/<secret>/open/<secret2>")
|
||||
async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
||||
|
||||
|
|
|
@ -67,6 +67,9 @@ SPONSORSHIP_COLOR_MAP = {
|
|||
'normal': (142, 36, 170)
|
||||
}
|
||||
|
||||
# Quotes
|
||||
QUOTES_LIST = []
|
||||
|
||||
# Maps Products metadata name <--> ID
|
||||
ITEMS_ID_MAP = {
|
||||
'early_bird_ticket': None,
|
||||
|
|
63
ext.py
63
ext.py
|
@ -247,6 +247,44 @@ class Order:
|
|||
|
||||
def get_language(self):
|
||||
return self.country.lower() if self.country.lower() in AVAILABLE_LOCALES else 'en'
|
||||
|
||||
def __str__(self):
|
||||
to_return = f"{'Room' if self.room_owner else 'Order'} {self.code}"
|
||||
if self.room_owner == True:
|
||||
to_return = f"{to_return} [ members = {self.room_members} ]"
|
||||
return to_return
|
||||
|
||||
def __repr__(self):
|
||||
to_return = f"{'Room' if self.room_owner == True else 'Order'} {self.code}"
|
||||
if self.room_owner == True:
|
||||
to_return = f"{to_return} [ members = {self.room_members} ]"
|
||||
return to_return
|
||||
|
||||
@dataclass
|
||||
class Quota:
|
||||
def __init__(self, data):
|
||||
self.items = data['items'] if 'items' in data else []
|
||||
self.variations = data['variations'] if 'variations' in data else []
|
||||
self.available = data['available'] if 'available' in data else False
|
||||
self.size = data['size'] if 'size' in data else 0
|
||||
self.available_number = data['available_number'] if 'available_number' in data else 0
|
||||
|
||||
def has_item (self, id: int=-1, variation: int=None):
|
||||
return id in self.items if not variation else (id in self.items and variation in self.variations)
|
||||
|
||||
def get_left (self):
|
||||
return self.available_number
|
||||
|
||||
def __repr__(self):
|
||||
return f'Quota [items={self.items}, variations={self.variations}] [{self.available_number}/{self.size}]'
|
||||
|
||||
def __str__(self):
|
||||
return f'Quota [items={self.items}, variations={self.variations}] [{self.available_number}/{self.size}]'
|
||||
|
||||
def get_quota(item: int, variation: int = None) -> Quota:
|
||||
for q in QUOTA_LIST:
|
||||
if (q.has_item(item, variation)): return q
|
||||
return None
|
||||
|
||||
@dataclass
|
||||
class Quotas:
|
||||
|
@ -265,6 +303,21 @@ async def get_quotas(request: Request=None):
|
|||
|
||||
return Quotas(res)
|
||||
|
||||
async def load_item_quotas() -> bool:
|
||||
global QUOTA_LIST
|
||||
QUOTA_LIST = []
|
||||
logger.info ('[QUOTAS] Loading quotas...')
|
||||
success = True
|
||||
try:
|
||||
res = await pretixClient.get('quotas/?order=id&with_availability=true')
|
||||
res = res.json()
|
||||
for quota_data in res['results']:
|
||||
QUOTA_LIST.append (Quota(quota_data))
|
||||
except Exception:
|
||||
logger.warning(f"[QUOTAS] Error while loading quotas.\n{traceback.format_exc()}")
|
||||
success = False
|
||||
return success
|
||||
|
||||
async def get_order(request: Request=None):
|
||||
await request.receive_body()
|
||||
return await request.app.ctx.om.get_order(request=request)
|
||||
|
@ -328,6 +381,12 @@ class OrderManager:
|
|||
logger.error("[CACHE] Questions were not loading correctly. Aborting filling cache...")
|
||||
return False
|
||||
|
||||
# Load quotas
|
||||
r = await load_item_quotas()
|
||||
if(not r and check_itemsQuestions):
|
||||
logger.error("[CACHE] Quotas were not loading correctly. Aborting filling cache...")
|
||||
return False
|
||||
|
||||
cache = {}
|
||||
orderList = []
|
||||
success = True
|
||||
|
@ -363,7 +422,7 @@ class OrderManager:
|
|||
asyncio.create_task(validate_rooms(None, rooms, self))
|
||||
|
||||
return success
|
||||
|
||||
|
||||
async def get_order(self, request=None, code=None, secret=None, nfc_id=None, cached=False):
|
||||
|
||||
# if it's a nfc id, just retorn it
|
||||
|
@ -405,4 +464,4 @@ class OrderManager:
|
|||
|
||||
if request and secret != res['secret']:
|
||||
raise exceptions.Forbidden("Your session has expired due to a token change. Please check your E-Mail for an updated link!")
|
||||
return order
|
||||
return order
|
BIN
res/botbg2.png
BIN
res/botbg2.png
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 19C13 20.1 13.3 21.12 13.81 22H6C4.89 22 4 21.11 4 20V4C4 2.9 4.89 2 6 2H7V9L9.5 7.5L12 9V2H18C19.1 2 20 2.89 20 4V13.09C19.67 13.04 19.34 13 19 13C15.69 13 13 15.69 13 19M20 18V15H18V18H15V20H18V23H20V20H23V18H20Z" /></svg>
|
Before Width: | Height: | Size: 297 B After Width: | Height: | Size: 297 B |
|
@ -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');
|
||||
}
|
|
@ -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 ();
|
|
@ -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;
|
||||
}
|
|
@ -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);}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -11,14 +11,20 @@
|
|||
</header>
|
||||
<!-- Quick controls -->
|
||||
<h2>Admin panel</h2>
|
||||
<p>Data</p>
|
||||
<a href="/manage/admin/cache/clear" role="button" title="Reload the orders' data and the re-sync items' indexes from pretix">Clear cache</a>
|
||||
<a href="/manage/nosecount" role="button" title="Shortcut to the nosecount's admin data">Manage rooms</a>
|
||||
<a href="/manage/admin/room/verify" role="button" title="Will unconfirm rooms that fail the default check. Useful when editing answers from Pretix">Verify Rooms</a>
|
||||
<a href="#" onclick="confirmAction('propicReminder', this)" role="button" title="Will remind via mail all people who event uploaded a badge to do it" action="/manage/admin/propic/remind">Remind badge upload</a>
|
||||
<a href="/manage/admin/room/autoconfirm" role="button" title="Will confirm all the full rooms that are still unconfirmed">Auto-confirm Rooms</a>
|
||||
<a download href="/manage/admin/export/export" role="button" title="Will export most of informations about the orders">Export CSV</a>
|
||||
<a download href="/manage/admin/export/hotel" role="button" title="Will export a CSV for the hotel accomodation">Export hotel CSV</a>
|
||||
<hr>
|
||||
<p>Rooms</p>
|
||||
<a href="/manage/nosecount" role="button" title="Shortcut to the nosecount's admin data">Manage rooms</a>
|
||||
<a href="/manage/admin/room/verify" role="button" title="Will unconfirm rooms that fail the default check. Useful when editing answers from Pretix">Verify Rooms</a>
|
||||
<a href="/manage/admin/room/wizard" role="button" title="Auto fill unconfirmed rooms. You can review and edit matches before confirming.">Fill Rooms</a>
|
||||
<hr>
|
||||
<p>Profiles</p>
|
||||
<a href="#" onclick="confirmAction('propicReminder', this)" role="button" title="Will email all people who haven't uploaded a badge, yet" action="/manage/admin/propic/remind">Remind badge upload</a>
|
||||
<a href="/manage/admin/room/autoconfirm" role="button" title="Will confirm all the full rooms that are still unconfirmed">Auto-confirm Rooms</a>
|
||||
<hr>
|
||||
{% include 'components/confirm_action_modal.html' %}
|
||||
</main>
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div aria-hidden="true" class="propic-border-animation"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if current and current.isAdmin () and (not current.code == order.code )%}
|
||||
{% if current and current.isAdmin () and (not current.code == order.code ) and not nologin %}
|
||||
<a class="control-login-as" href="/manage/admin/loginas/{{order.code}}">
|
||||
{% endif %}
|
||||
<img alt="{{order.ans('fursona_name') if order.ans('fursona_name') else 'A user'}}'s profile picture" src="{{imgSrc}}" class="absolute propic {{(('propic-' + order.sponsorship) if not effects else '') if order.sponsorship else 'propic-base'}}"/>
|
||||
|
|
|
@ -19,7 +19,9 @@
|
|||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
|
||||
<p>Welcome to the nosecount page! Here you can see all of the available rooms at the convention, as well as the occupants currently staying in each room. Use this page to find your friends and plan your meet-ups.</p>
|
||||
|
||||
{% if filtered and order %}
|
||||
{% for person in filtered.values() %}
|
||||
{% if loop.first %}
|
||||
|
@ -135,7 +137,7 @@
|
|||
{% endfor %}
|
||||
|
||||
<form id="intentFormAction" method="GET" action="">
|
||||
<dialog id="modalRoomconfirm">
|
||||
<dialog id="modalOrderEditDialog">
|
||||
<article>
|
||||
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
|
||||
<h3 id="intentText">Confirm room edit</h3>
|
||||
|
@ -150,6 +152,5 @@
|
|||
</article>
|
||||
</dialog>
|
||||
</form>
|
||||
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Room Wizard{% endblock %}
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/res/styles/wizard.css">
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<main class="container">
|
||||
<script src="/res/scripts/wizardManager.js" type="text/javascript" defer="defer"></script>
|
||||
<header>
|
||||
<picture>
|
||||
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
|
||||
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
<!--order = current order login
|
||||
unconfirmed_orders = all non confirmed rooms orders
|
||||
all_orders = all orders
|
||||
data = assigned rooms -->
|
||||
<h2>Review rooms <a href="#popover-empty-room-tip" onclick="document.querySelector('#popover-wizard-tip').showPopover()">?</a></h2>
|
||||
<div popover id="popover-wizard-tip">This is the preview page. Re-arrange users by dragging and dropping them in the rooms.<br>Once finished, scroll down to either <i>'Confirm'</i> changes or <i>'Undo'</i> them.</div>
|
||||
<hr>
|
||||
{% for room in data.items() %}
|
||||
{% if room[0] in all_orders %}
|
||||
{%with room_order = unconfirmed_orders[room[0]] %}
|
||||
<div class="room" id="room-{{room_order.code}}" room-type="{{room_order.bed_in_room}}" room-size="{{room_order.room_person_no - len(room_order.room_members)}}" current-size="{{len(room[1]['to_add'])}}">
|
||||
<h4 style="margin-top:1em;">
|
||||
<span>{{room_order.room_name if room_order.room_name else room[1]['room_name'] if room[1] and room[1]['room_name'] else ''}}</span>
|
||||
</h4>
|
||||
<div class="grid people" style="padding-bottom:1em;">
|
||||
{% for m in room_order.room_members %}
|
||||
{% if m in all_orders %}
|
||||
{% with person = all_orders[m] %}
|
||||
<div class="edit-disabled" style="margin-bottom: 1em;">
|
||||
{% with current=None, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for m in room[1]['to_add'] %}
|
||||
{% if m in unconfirmed_orders %}
|
||||
{% with person = unconfirmed_orders[m] %}
|
||||
<div class="edit-drag" id="{{person.code}}" room-type="{{person.bed_in_room}}" style="margin-bottom: 1em;" draggable="true">
|
||||
{% with current=None, order=person, imgSrc='/res/propic/' + (person.ans('propic') or 'default.png'), effects = false, flag = true %}
|
||||
{% include 'blocks/propic.html' %}
|
||||
{% endwith %}
|
||||
<h5>{{person.ans('fursona_name')}}</h5>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="room" infinite="true" id="room-infinite" room-size="999999999" current-size="0">
|
||||
<h4 style="margin-top:1em;">Empty room <a href="#popover-empty-room-tip" onclick="document.querySelector('#popover-empty-room-tip').showPopover()">?</a></button></h4>
|
||||
<div popover id="popover-empty-room-tip">This is a placeholder room. Place users temporarily in order to free space and arrange rooms</div>
|
||||
<div class="grid people" style="padding-bottom:1em;"></div>
|
||||
</div>
|
||||
<a href="/manage/admin" role="button" title="Discard all changes and go back to the admin page">Undo</a>
|
||||
<a href="#" class="align-right" onclick="onSave()" role="button" title="Will try saving current changes.">Confirm changes</a>
|
||||
|
||||
<dialog id="modalConfirmDialog">
|
||||
<article>
|
||||
<a href="#close" id="modalClose" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
|
||||
<h3 id="intentText">Confirm arrangement?</h3>
|
||||
<p id="intentDescription">
|
||||
Roomless guests will be moved around existing rooms and newly generated ones.<br>
|
||||
This will also confirm all rooms.
|
||||
</p>
|
||||
<div popover id="popover-status"><span id="popover-status-text"></span></div>
|
||||
<footer>
|
||||
<button id="intentSend" onclick="submitData(this)">Confirm</button>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
|
||||
<script type="text/javascript">
|
||||
let saveData = JSON.parse('{{jsondata|safe}}');
|
||||
</script>
|
||||
|
||||
</main>
|
||||
|
||||
{% endblock %}
|
5
utils.py
5
utils.py
|
@ -29,7 +29,6 @@ QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.htm
|
|||
}
|
||||
TYPE_OF_QUESTIONS = {} # maps questionId -> type
|
||||
|
||||
|
||||
async def load_questions() -> bool:
|
||||
global TYPE_OF_QUESTIONS
|
||||
# TYPE_OF_QUESTIONS.clear() It should not be needed
|
||||
|
@ -84,7 +83,7 @@ async def load_items() -> bool:
|
|||
ROOM_TYPE_NAMES[v['id']] = roomName
|
||||
# Adds itself to the category list
|
||||
categoryName = check_and_get_category ('item', q)
|
||||
if not categoryName: continue
|
||||
if not categoryName or q['id'] in CATEGORIES_LIST_MAP[categoryName]: continue
|
||||
CATEGORIES_LIST_MAP[categoryName].append(q['id'])
|
||||
if (EXTRA_PRINTS):
|
||||
logger.debug(f'Mapped Items: %s', ITEMS_ID_MAP)
|
||||
|
@ -149,7 +148,7 @@ async def get_order_by_code(request, code, throwException=False):
|
|||
async def get_people_in_room_by_code(request, code, om=None):
|
||||
if not om: om = request.app.ctx.om
|
||||
await om.update_cache()
|
||||
return filter(lambda rm: rm.room_id == code, om.cache.values())
|
||||
return list(filter(lambda rm: rm.room_id == code, om.cache.values()))
|
||||
|
||||
async def confirm_room_by_order(order, request):
|
||||
bed_in_room = order.bed_in_room # Variation id of the ticket for that kind of room
|
||||
|
|
Loading…
Reference in New Issue