Makeroom wizard #27
30
admin.py
30
admin.py
|
@ -118,35 +118,20 @@ async def room_wizard(request, order:Order):
|
||||||
roomless_orders = {key:value for key,value in orders.items() if not value.room_id and not value.daily}
|
roomless_orders = {key:value for key,value in orders.items() if not value.room_id and not value.daily}
|
||||||
|
|
||||||
# Result map
|
# Result map
|
||||||
result_map = {
|
|
||||||
'A':{
|
|
||||||
'type': 'add_existing',
|
|
||||||
'to_add': ['b', 'c']
|
|
||||||
},
|
|
||||||
'B':{
|
|
||||||
'type': 'new',
|
|
||||||
'room_type': 5,
|
|
||||||
'room_name': 'generated 1',
|
|
||||||
'to_add': ['B', 'a', 'c']
|
|
||||||
},
|
|
||||||
'rooms_to_delete': []
|
|
||||||
}
|
|
||||||
|
|
||||||
result_map = {}
|
result_map = {}
|
||||||
|
|
||||||
# Get room quotas
|
# Check overflows
|
||||||
room_quota_map = {}
|
|
||||||
room_quota_overflow = {}
|
room_quota_overflow = {}
|
||||||
for key, value in ITEM_VARIATIONS_MAP['bed_in_room'].items():
|
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
|
capacity = ROOM_CAPACITY_MAP[key] if key in ROOM_CAPACITY_MAP else 1
|
||||||
room_quota_map[value] = math.ceil((len(list(filter(lambda y: y.bed_in_room == value, orders.values())))) / capacity)
|
|
||||||
current_quota = len(list(filter(lambda y: y.bed_in_room == value and y.room_owner == True, orders.values())))
|
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 - (room_quota_map[value] if value in room_quota_map else 0)
|
room_quota_overflow[value] = current_quota - int(room_quota.size / capacity) if room_quota else 0
|
||||||
|
|
||||||
# Init rooms to remove
|
# Init rooms to remove
|
||||||
result_map["void"] = []
|
result_map["void"] = []
|
||||||
|
|
||||||
# Dismember rooms that are over quota
|
# 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():
|
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 = sorted(incomplete_orders.values(), key=lambda r: len(r.room_members))
|
||||||
for room_to_remove in sorted_rooms[:overflow_qty]:
|
for room_to_remove in sorted_rooms[:overflow_qty]:
|
||||||
|
@ -215,6 +200,13 @@ async def room_wizard(request, order:Order):
|
||||||
tpl = request.app.ctx.tpl.get_template('wizard.html')
|
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)))
|
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 submin_from_room_wizard(request:Request, order:Order):
|
||||||
|
'''Will apply changes to the rooms'''
|
||||||
|
print(request.body)
|
||||||
|
return text('Not implemented', status=500)
|
||||||
|
|
||||||
|
|
||||||
@bp.get('/propic/remind')
|
@bp.get('/propic/remind')
|
||||||
async def propic_remind_missing(request, order:Order):
|
async def propic_remind_missing(request, order:Order):
|
||||||
await clear_cache(request, order)
|
await clear_cache(request, order)
|
||||||
|
|
7
app.py
7
app.py
|
@ -49,7 +49,7 @@ async def handleException(request, exception):
|
||||||
statusCode = exception.status_code if hasattr(exception, 'status_code') else 500
|
statusCode = exception.status_code if hasattr(exception, 'status_code') else 500
|
||||||
try:
|
try:
|
||||||
tpl = app.ctx.tpl.get_template('error.html')
|
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:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
@ -98,6 +98,11 @@ async def gen_barcode(request, code):
|
||||||
|
|
||||||
return raw(img.getvalue(), content_type="image/png")
|
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>")
|
@app.route(f"/{ORGANIZER}/{EVENT_NAME}/order/<code>/<secret>/open/<secret2>")
|
||||||
async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,9 @@ SPONSORSHIP_COLOR_MAP = {
|
||||||
'normal': (142, 36, 170)
|
'normal': (142, 36, 170)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Quotes
|
||||||
|
QUOTES_LIST = []
|
||||||
|
|
||||||
# Maps Products metadata name <--> ID
|
# Maps Products metadata name <--> ID
|
||||||
ITEMS_ID_MAP = {
|
ITEMS_ID_MAP = {
|
||||||
'early_bird_ticket': None,
|
'early_bird_ticket': None,
|
||||||
|
|
51
ext.py
51
ext.py
|
@ -260,6 +260,32 @@ class Order:
|
||||||
to_return = f"{to_return} [ members = {self.room_members} ]"
|
to_return = f"{to_return} [ members = {self.room_members} ]"
|
||||||
return to_return
|
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
|
@dataclass
|
||||||
class Quotas:
|
class Quotas:
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
|
@ -277,6 +303,21 @@ async def get_quotas(request: Request=None):
|
||||||
|
|
||||||
return Quotas(res)
|
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):
|
async def get_order(request: Request=None):
|
||||||
await request.receive_body()
|
await request.receive_body()
|
||||||
return await request.app.ctx.om.get_order(request=request)
|
return await request.app.ctx.om.get_order(request=request)
|
||||||
|
@ -340,6 +381,12 @@ class OrderManager:
|
||||||
logger.error("[CACHE] Questions were not loading correctly. Aborting filling cache...")
|
logger.error("[CACHE] Questions were not loading correctly. Aborting filling cache...")
|
||||||
return False
|
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 = {}
|
cache = {}
|
||||||
orderList = []
|
orderList = []
|
||||||
success = True
|
success = True
|
||||||
|
@ -375,7 +422,7 @@ class OrderManager:
|
||||||
asyncio.create_task(validate_rooms(None, rooms, self))
|
asyncio.create_task(validate_rooms(None, rooms, self))
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
async def get_order(self, request=None, code=None, secret=None, nfc_id=None, cached=False):
|
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
|
# if it's a nfc id, just retorn it
|
||||||
|
@ -417,4 +464,4 @@ class OrderManager:
|
||||||
|
|
||||||
if request and secret != res['secret']:
|
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!")
|
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
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" /></svg>
|
|
|
@ -4,6 +4,8 @@ var draggingData = {
|
||||||
parentRoomId: 0
|
parentRoomId: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var allowRedirect = false;
|
||||||
|
|
||||||
function initObjects (){
|
function initObjects (){
|
||||||
draggables = document.querySelectorAll("div.grid.people div.edit-drag");
|
draggables = document.querySelectorAll("div.grid.people div.edit-drag");
|
||||||
rooms = document.querySelectorAll("main.container>div.room");
|
rooms = document.querySelectorAll("main.container>div.room");
|
||||||
|
@ -117,7 +119,7 @@ function getData () { return draggingData; }
|
||||||
|
|
||||||
// This default onbeforeunload event
|
// This default onbeforeunload event
|
||||||
window.onbeforeunload = function(){
|
window.onbeforeunload = function(){
|
||||||
return "Any changes to the rooms will be discarded."
|
if (!allowRedirect) return "Any changes to the rooms will be discarded."
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Model managing */
|
/* Model managing */
|
||||||
|
@ -152,4 +154,44 @@ function onSave (){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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 ();
|
initObjects ();
|
|
@ -27,6 +27,14 @@ summary:has(span.status) {
|
||||||
100% { background-position:57.75% 0%; }
|
100% { background-position:57.75% 0%; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Popover */
|
||||||
|
*[popover]:popover-open {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid #fff;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dark theme */
|
/* Dark theme */
|
||||||
@media only screen and (prefers-color-scheme: dark) {
|
@media only screen and (prefers-color-scheme: dark) {
|
||||||
.icon {filter: invert(1);}
|
.icon {filter: invert(1);}
|
||||||
|
|
|
@ -50,6 +50,14 @@ div.room:nth-child(2n) {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-success {
|
||||||
|
background-color: #2e9147aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-error {
|
||||||
|
background-color: #912e2eaa;
|
||||||
|
}
|
||||||
|
|
||||||
/* Dark theme */
|
/* Dark theme */
|
||||||
@media only screen and (prefers-color-scheme: dark) {
|
@media only screen and (prefers-color-scheme: dark) {
|
||||||
div.drag-over {
|
div.drag-over {
|
||||||
|
|
|
@ -19,10 +19,10 @@
|
||||||
<p>Rooms</p>
|
<p>Rooms</p>
|
||||||
<a href="/manage/nosecount" role="button" title="Shortcut to the nosecount's admin data">Manage rooms</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="/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="Will try matching roomless furs together, you can still re-arrange the users before confirming.">Fill 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>
|
<hr>
|
||||||
<p>Profiles</p>
|
<p>Profiles</p>
|
||||||
<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="#" 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>
|
<a href="/manage/admin/room/autoconfirm" role="button" title="Will confirm all the full rooms that are still unconfirmed">Auto-confirm Rooms</a>
|
||||||
<hr>
|
<hr>
|
||||||
{% include 'components/confirm_action_modal.html' %}
|
{% include 'components/confirm_action_modal.html' %}
|
||||||
|
|
|
@ -16,12 +16,13 @@
|
||||||
unconfirmed_orders = all non confirmed rooms orders
|
unconfirmed_orders = all non confirmed rooms orders
|
||||||
all_orders = all orders
|
all_orders = all orders
|
||||||
data = assigned rooms -->
|
data = assigned rooms -->
|
||||||
<h2>Review rooms</h2>
|
<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>
|
<hr>
|
||||||
{% for room in data.items() %}
|
{% for room in data.items() %}
|
||||||
{% if room[0] in all_orders %}
|
{% if room[0] in all_orders %}
|
||||||
{%with room_order = unconfirmed_orders[room[0]] %}
|
{%with room_order = unconfirmed_orders[room[0]] %}
|
||||||
<div class="room" id="room-{{room_order.code}}" room-type="{{room_order.bed_in_room}}" room-size="{{len(room[1]['to_add'])}}" current-size="{{len(room[1]['to_add'])}}">
|
<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;">
|
<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>
|
<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>
|
</h4>
|
||||||
|
@ -65,14 +66,15 @@
|
||||||
|
|
||||||
<dialog id="modalConfirmDialog">
|
<dialog id="modalConfirmDialog">
|
||||||
<article>
|
<article>
|
||||||
<a href="#close" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
|
<a href="#close" id="modalClose" aria-label="Close" class="close" onClick="javascript:this.parentElement.parentElement.removeAttribute('open')"></a>
|
||||||
<h3 id="intentText">Confirm arrangement?</h3>
|
<h3 id="intentText">Confirm arrangement?</h3>
|
||||||
<p id="intentDescription">
|
<p id="intentDescription">
|
||||||
Roomless guests will be moved around existing rooms and newly generated ones.<br>
|
Roomless guests will be moved around existing rooms and newly generated ones.<br>
|
||||||
This will also confirm all rooms.
|
This will also confirm all rooms.
|
||||||
</p>
|
</p>
|
||||||
|
<div popover id="popover-status"><span id="popover-status-text"></span></div>
|
||||||
<footer>
|
<footer>
|
||||||
<input id="intentSend" type="submit" value="Confirm" />
|
<button id="intentSend" onclick="submitData(this)">Confirm</button>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
1
utils.py
1
utils.py
|
@ -29,7 +29,6 @@ QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.htm
|
||||||
}
|
}
|
||||||
TYPE_OF_QUESTIONS = {} # maps questionId -> type
|
TYPE_OF_QUESTIONS = {} # maps questionId -> type
|
||||||
|
|
||||||
|
|
||||||
async def load_questions() -> bool:
|
async def load_questions() -> bool:
|
||||||
global TYPE_OF_QUESTIONS
|
global TYPE_OF_QUESTIONS
|
||||||
# TYPE_OF_QUESTIONS.clear() It should not be needed
|
# TYPE_OF_QUESTIONS.clear() It should not be needed
|
||||||
|
|
Loading…
Reference in New Issue