Merge pull request 'We're so back' (#16) from drew-dev into stranck-dev
Reviewed-on: #16
This commit is contained in:
commit
e780a2a0c0
|
@ -154,6 +154,8 @@ cython_debug/
|
||||||
|
|
||||||
res/propic/propic_*
|
res/propic/propic_*
|
||||||
|
|
||||||
|
res/rooms/*
|
||||||
|
|
||||||
# PyCharm
|
# PyCharm
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
|
60
admin.py
60
admin.py
|
@ -1,41 +1,36 @@
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from sanic import response, redirect, Blueprint, exceptions
|
from sanic import response, redirect, Blueprint, exceptions
|
||||||
|
from room import unconfirm_room_by_order
|
||||||
from config import *
|
from config import *
|
||||||
from utils import *
|
from utils import *
|
||||||
from ext import *
|
from ext import *
|
||||||
import sqlite3
|
from sanic.log import logger
|
||||||
import smtplib
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import httpx
|
|
||||||
import json
|
|
||||||
|
|
||||||
bp = Blueprint("admin", url_prefix="/manage/admin")
|
bp = Blueprint("admin", url_prefix="/manage/admin")
|
||||||
|
|
||||||
def credentialsCheck(request, order:Order):
|
@bp.middleware
|
||||||
|
async def credentials_check(request: Request):
|
||||||
|
order = await get_order(request)
|
||||||
if not order:
|
if not order:
|
||||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||||
if EXTRA_PRINTS:
|
if EXTRA_PRINTS:
|
||||||
print(f"Checking admin credentials of {order.code} with secret {order.secret}")
|
logger.info(f"Checking admin credentials of {order.code} with secret {order.secret}")
|
||||||
if not order.isAdmin() : raise exceptions.Forbidden("Birichino :)")
|
if not order.isAdmin() : raise exceptions.Forbidden("Birichino :)")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get('/cache/clear')
|
@bp.get('/cache/clear')
|
||||||
async def clearCache(request, order:Order):
|
async def clear_cache(request, order:Order):
|
||||||
credentialsCheck(request, order)
|
|
||||||
await request.app.ctx.om.fill_cache()
|
await request.app.ctx.om.fill_cache()
|
||||||
return redirect(f'/manage/admin')
|
return redirect(f'/manage/admin')
|
||||||
|
|
||||||
@bp.get('/loginas/<code>')
|
@bp.get('/loginas/<code>')
|
||||||
async def loginAs(request, code, order:Order):
|
async def login_as(request, code, order:Order):
|
||||||
credentialsCheck(request, order)
|
dOrder = await get_order_by_code(request, code, throwException=True)
|
||||||
dOrder = await getOrderByCode(request, code, throwException=True)
|
|
||||||
if(dOrder.isAdmin()):
|
if(dOrder.isAdmin()):
|
||||||
raise exceptions.Forbidden("You can't login as another admin!")
|
raise exceptions.Forbidden("You can't login as another admin!")
|
||||||
|
|
||||||
if EXTRA_PRINTS:
|
if EXTRA_PRINTS:
|
||||||
print(f"Swapping login: {order.secret} {order.code} -> {dOrder.secret} {code}")
|
logger.info(f"Swapping login: {order.secret} {order.code} -> {dOrder.secret} {code}")
|
||||||
r = redirect(f'/manage/welcome')
|
r = redirect(f'/manage/welcome')
|
||||||
r.cookies['foxo_code_ORG'] = order.code
|
r.cookies['foxo_code_ORG'] = order.code
|
||||||
r.cookies['foxo_secret_ORG'] = order.secret
|
r.cookies['foxo_secret_ORG'] = order.secret
|
||||||
|
@ -43,27 +38,25 @@ async def loginAs(request, code, order:Order):
|
||||||
r.cookies['foxo_secret'] = dOrder.secret
|
r.cookies['foxo_secret'] = dOrder.secret
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
@bp.get('/room/verify')
|
||||||
|
async def verify_rooms(request, order:Order):
|
||||||
|
already_checked = await request.app.ctx.om.update_cache()
|
||||||
|
if not already_checked:
|
||||||
|
orders = filter(lambda x: x.status not in ['c', 'e'] and x.room_id == x.code, request.app.ctx.om.cache.values())
|
||||||
|
await validate_rooms(request, orders, None)
|
||||||
|
return redirect(f'/manage/admin')
|
||||||
|
|
||||||
@bp.get('/room/unconfirm/<code>')
|
@bp.get('/room/unconfirm/<code>')
|
||||||
async def unconfirmRoom(request, code, order:Order):
|
async def unconfirm_room(request, code, order:Order):
|
||||||
credentialsCheck(request, order)
|
dOrder = await get_order_by_code(request, code, throwException=True)
|
||||||
dOrder = await getOrderByCode(request, code, throwException=True)
|
await unconfirm_room_by_order(order=dOrder, throw=True, request=request)
|
||||||
|
|
||||||
if(not dOrder.room_confirmed):
|
|
||||||
raise exceptions.BadRequest("Room is not confirmed!")
|
|
||||||
|
|
||||||
ppl = getPeopleInRoomByRoomId(request, code)
|
|
||||||
for p in ppl:
|
|
||||||
await p.edit_answer('room_confirmed', "False")
|
|
||||||
await p.send_answers()
|
|
||||||
|
|
||||||
return redirect(f'/manage/nosecount')
|
return redirect(f'/manage/nosecount')
|
||||||
|
|
||||||
@bp.get('/room/delete/<code>')
|
@bp.get('/room/delete/<code>')
|
||||||
async def deleteRoom(request, code, order:Order):
|
async def delete_room(request, code, order:Order):
|
||||||
credentialsCheck(request, order)
|
dOrder = await get_order_by_code(request, code, throwException=True)
|
||||||
dOrder = await getOrderByCode(request, code, throwException=True)
|
|
||||||
|
|
||||||
ppl = getPeopleInRoomByRoomId(request, code)
|
ppl = await get_people_in_room_by_code(request, code)
|
||||||
for p in ppl:
|
for p in ppl:
|
||||||
await p.edit_answer('room_id', None)
|
await p.edit_answer('room_id', None)
|
||||||
await p.edit_answer('room_confirmed', "False")
|
await p.edit_answer('room_confirmed', "False")
|
||||||
|
@ -79,9 +72,8 @@ async def deleteRoom(request, code, order:Order):
|
||||||
return redirect(f'/manage/nosecount')
|
return redirect(f'/manage/nosecount')
|
||||||
|
|
||||||
@bp.post('/room/rename/<code>')
|
@bp.post('/room/rename/<code>')
|
||||||
async def renameRoom(request, code, order:Order):
|
async def rename_room(request, code, order:Order):
|
||||||
credentialsCheck(request, order)
|
dOrder = await get_order_by_code(request, code, throwException=True)
|
||||||
dOrder = await getOrderByCode(request, code, throwException=True)
|
|
||||||
|
|
||||||
name = request.form.get('name')
|
name = request.form.get('name')
|
||||||
if len(name) > 64 or len(name) < 4:
|
if len(name) > 64 or len(name) < 4:
|
||||||
|
|
4
api.py
4
api.py
|
@ -9,6 +9,7 @@ import random
|
||||||
import string
|
import string
|
||||||
import httpx
|
import httpx
|
||||||
import json
|
import json
|
||||||
|
from sanic.log import logger
|
||||||
|
|
||||||
bp = Blueprint("api", url_prefix="/manage/api")
|
bp = Blueprint("api", url_prefix="/manage/api")
|
||||||
|
|
||||||
|
@ -185,7 +186,8 @@ async def nfc_scan(request, nfc_id):
|
||||||
@bp.get("/get_token/<code>/<login_code>")
|
@bp.get("/get_token/<code>/<login_code>")
|
||||||
async def get_token_from_code(request, code, login_code):
|
async def get_token_from_code(request, code, login_code):
|
||||||
if not code in request.app.ctx.login_codes:
|
if not code in request.app.ctx.login_codes:
|
||||||
print(request.app.ctx.login_codes)
|
if DEV_MODE and EXTRA_PRINTS:
|
||||||
|
logger.debug(request.app.ctx.login_codes)
|
||||||
return response.json({'ok': False, 'error': 'You need to reauthenticate. The code has expired.'}, status=401)
|
return response.json({'ok': False, 'error': 'You need to reauthenticate. The code has expired.'}, status=401)
|
||||||
|
|
||||||
if request.app.ctx.login_codes[code][1] == 0:
|
if request.app.ctx.login_codes[code][1] == 0:
|
||||||
|
|
28
app.py
28
app.py
|
@ -3,9 +3,6 @@ from sanic.response import text, html, redirect, raw
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from time import time
|
from time import time
|
||||||
import httpx
|
import httpx
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from os.path import join
|
from os.path import join
|
||||||
from ext import *
|
from ext import *
|
||||||
from config import *
|
from config import *
|
||||||
|
@ -13,9 +10,9 @@ from aztec_code_generator import AztecCode
|
||||||
from propic import resetDefaultPropic
|
from propic import resetDefaultPropic
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from asyncio import Queue
|
from asyncio import Queue
|
||||||
|
from messages import LOCALES
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
from sanic.log import logger
|
||||||
log = logging.getLogger()
|
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
app.static("/res", "res/")
|
app.static("/res", "res/")
|
||||||
|
@ -47,15 +44,13 @@ async def clear_session(request, exception):
|
||||||
|
|
||||||
@app.before_server_start
|
@app.before_server_start
|
||||||
async def main_start(*_):
|
async def main_start(*_):
|
||||||
print(">>>>>> main_start <<<<<<")
|
logger.info(f"[{app.name}] >>>>>> main_start <<<<<<")
|
||||||
|
|
||||||
app.config.REQUEST_MAX_SIZE = PROPIC_MAX_FILE_SIZE * 3
|
app.config.REQUEST_MAX_SIZE = PROPIC_MAX_FILE_SIZE * 3
|
||||||
|
|
||||||
app.ctx.om = OrderManager()
|
app.ctx.om = OrderManager()
|
||||||
if FILL_CACHE:
|
if FILL_CACHE:
|
||||||
log.info("Filling cache!")
|
await app.ctx.om.update_cache()
|
||||||
await app.ctx.om.updateCache()
|
|
||||||
log.info("Cache fill done!")
|
|
||||||
|
|
||||||
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
|
app.ctx.nfc_counts = sqlite3.connect('data/nfc_counts.db')
|
||||||
|
|
||||||
|
@ -64,6 +59,7 @@ async def main_start(*_):
|
||||||
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
|
app.ctx.tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=True)
|
||||||
app.ctx.tpl.globals.update(time=time)
|
app.ctx.tpl.globals.update(time=time)
|
||||||
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
|
app.ctx.tpl.globals.update(PROPIC_DEADLINE=PROPIC_DEADLINE)
|
||||||
|
app.ctx.tpl.globals.update(LOCALES=LOCALES)
|
||||||
app.ctx.tpl.globals.update(ITEMS_ID_MAP=ITEMS_ID_MAP)
|
app.ctx.tpl.globals.update(ITEMS_ID_MAP=ITEMS_ID_MAP)
|
||||||
app.ctx.tpl.globals.update(ITEM_VARIATIONS_MAP=ITEM_VARIATIONS_MAP)
|
app.ctx.tpl.globals.update(ITEM_VARIATIONS_MAP=ITEM_VARIATIONS_MAP)
|
||||||
app.ctx.tpl.globals.update(ROOM_TYPE_NAMES=ROOM_TYPE_NAMES)
|
app.ctx.tpl.globals.update(ROOM_TYPE_NAMES=ROOM_TYPE_NAMES)
|
||||||
|
@ -90,7 +86,7 @@ async def redirect_explore(request, code, secret, order: Order, secret2=None):
|
||||||
if not order:
|
if not order:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
res = await client.get(join(base_url_event, f"orders/{code}/"), headers=headers)
|
res = await client.get(join(base_url_event, f"orders/{code}/"), headers=headers)
|
||||||
print(res.json())
|
|
||||||
if res.status_code != 200:
|
if res.status_code != 200:
|
||||||
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")
|
raise exceptions.NotFound("This order code does not exist. Check that your order wasn't deleted, or the link is correct.")
|
||||||
|
|
||||||
|
@ -137,10 +133,10 @@ async def welcome(request, order: Order, quota: Quotas):
|
||||||
if member_id == order.code:
|
if member_id == order.code:
|
||||||
room_members.append(order)
|
room_members.append(order)
|
||||||
else:
|
else:
|
||||||
room_members.append(await app.ctx.om.get_order(code=member_id, cached=True))
|
room_members.append(await app.ctx.om.get_order(code=member_id, cached=True))
|
||||||
|
|
||||||
tpl = app.ctx.tpl.get_template('welcome.html')
|
tpl = app.ctx.tpl.get_template('welcome.html')
|
||||||
return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates))
|
return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates, ROOM_ERROR_MESSAGES=ROOM_ERROR_TYPES))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/manage/download_ticket")
|
@app.route("/manage/download_ticket")
|
||||||
|
@ -164,17 +160,17 @@ async def download_ticket(request, order: Order):
|
||||||
|
|
||||||
@app.route("/manage/admin")
|
@app.route("/manage/admin")
|
||||||
async def admin(request, order: Order):
|
async def admin(request, order: Order):
|
||||||
await request.app.ctx.om.updateCache()
|
await request.app.ctx.om.update_cache()
|
||||||
if not order:
|
if not order:
|
||||||
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||||
if EXTRA_PRINTS:
|
if EXTRA_PRINTS:
|
||||||
print(f"Checking admin credentials of {order.code} with secret {order.secret}")
|
logger.info(f"Checking admin credentials of {order.code} with secret {order.secret}")
|
||||||
if not order.isAdmin(): raise exceptions.Forbidden("Birichino :)")
|
if not order.isAdmin(): raise exceptions.Forbidden("Birichino :)")
|
||||||
tpl = app.ctx.tpl.get_template('admin.html')
|
tpl = app.ctx.tpl.get_template('admin.html')
|
||||||
return html(tpl.render(order=order))
|
return html(tpl.render(order=order))
|
||||||
|
|
||||||
@app.route("/manage/logout")
|
@app.route("/manage/logout")
|
||||||
async def logour(request):
|
async def logout(request):
|
||||||
orgCode = request.cookies.get("foxo_code_ORG")
|
orgCode = request.cookies.get("foxo_code_ORG")
|
||||||
orgSecret = request.cookies.get("foxo_secret_ORG")
|
orgSecret = request.cookies.get("foxo_secret_ORG")
|
||||||
if orgCode != None and orgSecret != None:
|
if orgCode != None and orgSecret != None:
|
||||||
|
@ -183,8 +179,6 @@ async def logour(request):
|
||||||
r.cookies['foxo_secret'] = orgSecret
|
r.cookies['foxo_secret'] = orgSecret
|
||||||
r.delete_cookie("foxo_code_ORG")
|
r.delete_cookie("foxo_code_ORG")
|
||||||
r.delete_cookie("foxo_secret_ORG")
|
r.delete_cookie("foxo_secret_ORG")
|
||||||
del r.cookies['foxo_code_ORG']
|
|
||||||
del r.cookies['foxo_secret_ORG']
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
raise exceptions.Forbidden("You have been logged out.")
|
raise exceptions.Forbidden("You have been logged out.")
|
||||||
|
|
|
@ -10,7 +10,6 @@ async def carpooling_list(request, order: Order, error=None):
|
||||||
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
if not order: raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
|
||||||
|
|
||||||
orders = [value for value in request.app.ctx.om.cache.values() if value.status not in ['c', 'e'] and value.carpooling_message]
|
orders = [value for value in request.app.ctx.om.cache.values() if value.status not in ['c', 'e'] and value.carpooling_message]
|
||||||
print(orders)
|
|
||||||
|
|
||||||
tpl = request.app.ctx.tpl.get_template('carpooling.html')
|
tpl = request.app.ctx.tpl.get_template('carpooling.html')
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,20 @@ PROPIC_MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
|
||||||
PROPIC_MAX_SIZE = (2048, 2048) # (Width, Height)
|
PROPIC_MAX_SIZE = (2048, 2048) # (Width, Height)
|
||||||
PROPIC_MIN_SIZE = (125, 125) # (Width, Height)
|
PROPIC_MIN_SIZE = (125, 125) # (Width, Height)
|
||||||
|
|
||||||
|
# This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id.
|
||||||
|
TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
||||||
|
TG_CHAT_ID = -1234567
|
||||||
|
|
||||||
|
# These order codes have additional functions.
|
||||||
|
ADMINS = ['XXXXX', 'YYYYY']
|
||||||
|
# A list of staff_roles
|
||||||
|
ADMINS_PRETIX_ROLE_NAMES = ["Reserved Area admin", "main staff"]
|
||||||
|
|
||||||
|
SMTP_HOST = 'host'
|
||||||
|
SMTP_PORT = 0
|
||||||
|
SMTP_USER = 'user'
|
||||||
|
SMTP_PASSWORD = 'pw'
|
||||||
|
|
||||||
FILL_CACHE = True
|
FILL_CACHE = True
|
||||||
CACHE_EXPIRE_TIME = 60 * 60 * 4
|
CACHE_EXPIRE_TIME = 60 * 60 * 4
|
||||||
|
|
||||||
|
@ -19,11 +33,21 @@ DEV_MODE = True
|
||||||
ACCESS_LOG = True
|
ACCESS_LOG = True
|
||||||
EXTRA_PRINTS = True
|
EXTRA_PRINTS = True
|
||||||
|
|
||||||
|
# Additional configured locales.
|
||||||
|
# If an order has a country that's not listed here,
|
||||||
|
# Will default to an english preference.
|
||||||
|
AVAILABLE_LOCALES = ['it',]
|
||||||
|
|
||||||
# Metadata property for item-id mapping
|
# Metadata property for item-id mapping
|
||||||
METADATA_NAME = "item_name"
|
METADATA_NAME = "item_name"
|
||||||
# Metadata property for internal category mapping (not related to pretix's category)
|
# Metadata property for internal category mapping (not related to pretix's category)
|
||||||
METADATA_CATEGORY = "category_name"
|
METADATA_CATEGORY = "category_name"
|
||||||
|
|
||||||
|
SPONSORSHIP_COLOR_MAP = {
|
||||||
|
'super': (251, 140, 0),
|
||||||
|
'normal': (142, 36, 170)
|
||||||
|
}
|
||||||
|
|
||||||
# Maps Products metadata name <--> ID
|
# Maps Products metadata name <--> ID
|
||||||
ITEMS_ID_MAP = {
|
ITEMS_ID_MAP = {
|
||||||
'early_bird_ticket': 126,
|
'early_bird_ticket': 126,
|
||||||
|
@ -86,6 +110,12 @@ CATEGORIES_LIST_MAP = {
|
||||||
'rooms': [],
|
'rooms': [],
|
||||||
'dailys': []
|
'dailys': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SPONSORSHIP_COLOR_MAP = {
|
||||||
|
'super': (251, 140, 0),
|
||||||
|
'normal': (142, 36, 170)
|
||||||
|
}
|
||||||
|
|
||||||
# Create a bunch of "room" items which will get added to the order once somebody gets a room.
|
# Create a bunch of "room" items which will get added to the order once somebody gets a room.
|
||||||
# Map item_name -> room capacity
|
# Map item_name -> room capacity
|
||||||
ROOM_CAPACITY_MAP = {
|
ROOM_CAPACITY_MAP = {
|
||||||
|
@ -101,15 +131,4 @@ ROOM_CAPACITY_MAP = {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Autofilled
|
# Autofilled
|
||||||
ROOM_TYPE_NAMES = { }
|
ROOM_TYPE_NAMES = { }
|
||||||
|
|
||||||
# This is used for feedback sending inside of the app. Feedbacks will be sent to the specified chat using the bot api id.
|
|
||||||
TG_BOT_API = '123456789:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
|
||||||
TG_CHAT_ID = -1234567
|
|
||||||
|
|
||||||
# These order codes have additional functions.
|
|
||||||
ADMINS = ['XXXXX', 'YYYYY']
|
|
||||||
|
|
||||||
SMTP_HOST = 'host'
|
|
||||||
SMTP_USER = 'user'
|
|
||||||
SMTP_PASSWORD = 'pw'
|
|
|
@ -8,13 +8,11 @@ from ext import *
|
||||||
from config import *
|
from config import *
|
||||||
from sanic import response
|
from sanic import response
|
||||||
from sanic import Blueprint
|
from sanic import Blueprint
|
||||||
import logging
|
from sanic.log import logger
|
||||||
|
|
||||||
log = logging.getLogger()
|
|
||||||
|
|
||||||
def checkConfig():
|
def checkConfig():
|
||||||
if (not DEV_MODE) and DUMMY_DATA:
|
if (not DEV_MODE) and DUMMY_DATA:
|
||||||
log.warn('It is strongly unadvised to use dummy data in production')
|
logger.warn('It is strongly unadvised to use dummy data in production')
|
||||||
|
|
||||||
def getOrders(page):
|
def getOrders(page):
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from messages import ROOM_ERROR_TYPES
|
||||||
|
import smtplib
|
||||||
|
from messages import *
|
||||||
|
from config import SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
async def send_unconfirm_message (room_order, orders):
|
||||||
|
memberMessages = []
|
||||||
|
|
||||||
|
issues_plain = ""
|
||||||
|
issues_html = "<ul>"
|
||||||
|
|
||||||
|
for err in room_order.room_errors:
|
||||||
|
if err in ROOM_ERROR_TYPES.keys():
|
||||||
|
issues_plain += f" • {ROOM_ERROR_TYPES[err]}\n"
|
||||||
|
issues_html += f"<li>{ROOM_ERROR_TYPES[err]}</li>"
|
||||||
|
issues_html += "</ul>"
|
||||||
|
|
||||||
|
for member in orders:
|
||||||
|
plain_body = ROOM_UNCONFIRM_TEXT['plain'].format(member.name, room_order.room_name, issues_plain)
|
||||||
|
html_body = render_email_template(ROOM_UNCONFIRM_TITLE, ROOM_UNCONFIRM_TEXT['html'].format(member.name, room_order.room_name, issues_html))
|
||||||
|
plain_text = MIMEText(plain_body, "plain")
|
||||||
|
html_text = MIMEText(html_body, "html")
|
||||||
|
message = MIMEMultipart("alternative")
|
||||||
|
message.attach(plain_text)
|
||||||
|
message.attach(html_text)
|
||||||
|
message['Subject'] = '[Furizon] Your room cannot be confirmed'
|
||||||
|
message['From'] = 'Furizon <no-reply@furizon.net>'
|
||||||
|
message['To'] = f"{member.name} <{member.email}>"
|
||||||
|
memberMessages.append(message)
|
||||||
|
|
||||||
|
if len(memberMessages) == 0: return
|
||||||
|
|
||||||
|
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as sender:
|
||||||
|
sender.login(SMTP_USER, SMTP_PASSWORD)
|
||||||
|
for message in memberMessages:
|
||||||
|
sender.sendmail(message['From'], message['to'], message.as_string())
|
||||||
|
|
||||||
|
def render_email_template(title = "", body = ""):
|
||||||
|
tpl = Environment(loader=FileSystemLoader("tpl"), autoescape=False).get_template('email/comunication.html')
|
||||||
|
return str(tpl.render(title=title, body=body))
|
94
ext.py
94
ext.py
|
@ -1,12 +1,14 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from sanic import Request, exceptions
|
from sanic import Request, exceptions, Sanic
|
||||||
import httpx
|
import httpx
|
||||||
import re
|
import re
|
||||||
from utils import *
|
from utils import *
|
||||||
from config import *
|
from config import *
|
||||||
from os.path import join
|
from os.path import join
|
||||||
import json
|
import json
|
||||||
|
from sanic.log import logger
|
||||||
from time import time
|
from time import time
|
||||||
|
import asyncio
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Order:
|
class Order:
|
||||||
|
@ -65,7 +67,7 @@ class Order:
|
||||||
self.has_card = True
|
self.has_card = True
|
||||||
|
|
||||||
if p['item'] == ITEMS_ID_MAP['sponsorship_item']:
|
if p['item'] == ITEMS_ID_MAP['sponsorship_item']:
|
||||||
sponsorshipType = keyFromValue(ITEM_VARIATIONS_MAP['sponsorship_item'], p['variation'])
|
sponsorshipType = key_from_value(ITEM_VARIATIONS_MAP['sponsorship_item'], p['variation'])
|
||||||
self.sponsorship = sponsorshipType[0].replace ('sponsorship_item_', '') if len(sponsorshipType) > 0 else None
|
self.sponsorship = sponsorshipType[0].replace ('sponsorship_item_', '') if len(sponsorshipType) > 0 else None
|
||||||
|
|
||||||
if p['attendee_name']:
|
if p['attendee_name']:
|
||||||
|
@ -79,7 +81,7 @@ class Order:
|
||||||
self.has_late = True
|
self.has_late = True
|
||||||
|
|
||||||
if p['item'] == ITEMS_ID_MAP['bed_in_room']:
|
if p['item'] == ITEMS_ID_MAP['bed_in_room']:
|
||||||
roomTypeLst = keyFromValue(ITEM_VARIATIONS_MAP['bed_in_room'], p['variation'])
|
roomTypeLst = key_from_value(ITEM_VARIATIONS_MAP['bed_in_room'], p['variation'])
|
||||||
roomTypeId = roomTypeLst[0] if len(roomTypeLst) > 0 else None
|
roomTypeId = roomTypeLst[0] if len(roomTypeLst) > 0 else None
|
||||||
self.bed_in_room = p['variation']
|
self.bed_in_room = p['variation']
|
||||||
self.room_person_no = ROOM_CAPACITY_MAP[roomTypeId] if roomTypeId in ROOM_CAPACITY_MAP else None
|
self.room_person_no = ROOM_CAPACITY_MAP[roomTypeId] if roomTypeId in ROOM_CAPACITY_MAP else None
|
||||||
|
@ -98,6 +100,7 @@ class Order:
|
||||||
self.payment_provider = data['payment_provider']
|
self.payment_provider = data['payment_provider']
|
||||||
self.comment = data['comment']
|
self.comment = data['comment']
|
||||||
self.phone = data['phone']
|
self.phone = data['phone']
|
||||||
|
self.room_errors = []
|
||||||
self.loadAns()
|
self.loadAns()
|
||||||
def loadAns(self):
|
def loadAns(self):
|
||||||
self.shirt_size = self.ans('shirt_size')
|
self.shirt_size = self.ans('shirt_size')
|
||||||
|
@ -128,8 +131,12 @@ class Order:
|
||||||
self.actual_room = self.ans('actual_room')
|
self.actual_room = self.ans('actual_room')
|
||||||
self.staff_role = self.ans('staff_role')
|
self.staff_role = self.ans('staff_role')
|
||||||
self.telegram_username = self.ans('telegram_username').strip('@') if self.ans('telegram_username') else None
|
self.telegram_username = self.ans('telegram_username').strip('@') if self.ans('telegram_username') else None
|
||||||
|
self.shuttle_bus = self.ans('shuttle_bus')
|
||||||
def __getitem__(self, var):
|
def __getitem__(self, var):
|
||||||
return self.data[var]
|
return self.data[var]
|
||||||
|
|
||||||
|
def set_room_errors (self, to_set):
|
||||||
|
for s in to_set: self.room_errors.append (s)
|
||||||
|
|
||||||
def ans(self, name):
|
def ans(self, name):
|
||||||
for p in self.data['positions']:
|
for p in self.data['positions']:
|
||||||
|
@ -165,11 +172,11 @@ class Order:
|
||||||
for key in range(len(self.answers)):
|
for key in range(len(self.answers)):
|
||||||
if self.answers[key].get('question_identifier', None) == name:
|
if self.answers[key].get('question_identifier', None) == name:
|
||||||
if new_answer != None:
|
if new_answer != None:
|
||||||
print('EXISTING ANSWER UPDATE', name, '=>', new_answer)
|
if DEV_MODE and EXTRA_PRINTS: logger.debug('[ANSWER EDIT] EXISTING ANSWER UPDATE %s => %s', name, new_answer)
|
||||||
self.answers[key]['answer'] = new_answer
|
self.answers[key]['answer'] = new_answer
|
||||||
found = True
|
found = True
|
||||||
else:
|
else:
|
||||||
print('DEL ANSWER', name, '=>', new_answer)
|
if DEV_MODE and EXTRA_PRINTS: logger.debug('[ANSWER EDIT] DEL ANSWER %s => %s', name, new_answer)
|
||||||
del self.answers[key]
|
del self.answers[key]
|
||||||
|
|
||||||
break
|
break
|
||||||
|
@ -182,7 +189,7 @@ class Order:
|
||||||
for r in res['results']:
|
for r in res['results']:
|
||||||
if r['identifier'] != name: continue
|
if r['identifier'] != name: continue
|
||||||
|
|
||||||
print('ANSWER UPDATE', name, '=>', new_answer)
|
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'[ANSWER EDIT] %s => %s', name, new_answer)
|
||||||
self.answers.append({
|
self.answers.append({
|
||||||
'question': r['id'],
|
'question': r['id'],
|
||||||
'answer': new_answer,
|
'answer': new_answer,
|
||||||
|
@ -192,7 +199,7 @@ class Order:
|
||||||
|
|
||||||
async def send_answers(self):
|
async def send_answers(self):
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
print("POSITION ID IS", self.position_id)
|
if DEV_MODE and EXTRA_PRINTS: logger.debug("[ANSWER POST] POSITION ID IS %s", self.position_id)
|
||||||
|
|
||||||
for i, ans in enumerate(self.answers):
|
for i, ans in enumerate(self.answers):
|
||||||
if TYPE_OF_QUESTIONS[ans['question']] == QUESTION_TYPES["multiple_choice_from_list"]: # if multiple choice
|
if TYPE_OF_QUESTIONS[ans['question']] == QUESTION_TYPES["multiple_choice_from_list"]: # if multiple choice
|
||||||
|
@ -209,7 +216,7 @@ class Order:
|
||||||
if res.status_code != 200:
|
if res.status_code != 200:
|
||||||
for ans, err in zip(self.answers, res.json()['answers']):
|
for ans, err in zip(self.answers, res.json()['answers']):
|
||||||
if err:
|
if err:
|
||||||
print('ERROR ON', ans, err)
|
logger.error ('[ANSWERS SENDING] ERROR ON %s %s', ans, err)
|
||||||
|
|
||||||
raise exceptions.ServerError('There has been an error while updating this answers.')
|
raise exceptions.ServerError('There has been an error while updating this answers.')
|
||||||
|
|
||||||
|
@ -220,6 +227,9 @@ class Order:
|
||||||
self.pending_update = False
|
self.pending_update = False
|
||||||
self.time = -1
|
self.time = -1
|
||||||
self.loadAns()
|
self.loadAns()
|
||||||
|
|
||||||
|
def get_language(self):
|
||||||
|
return self.country.lower() if self.country.lower() in AVAILABLE_LOCALES else 'en'
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Quotas:
|
class Quotas:
|
||||||
|
@ -246,18 +256,21 @@ async def get_order(request: Request=None):
|
||||||
class OrderManager:
|
class OrderManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.lastCacheUpdate = 0
|
self.lastCacheUpdate = 0
|
||||||
|
self.updating = False
|
||||||
self.empty()
|
self.empty()
|
||||||
|
|
||||||
def empty(self):
|
def empty(self):
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
self.order_list = []
|
self.order_list = []
|
||||||
|
|
||||||
async def updateCache(self):
|
# Will fill cache once the last cache update is greater than cache expire time
|
||||||
|
async def update_cache(self):
|
||||||
t = time()
|
t = time()
|
||||||
if(t - self.lastCacheUpdate > CACHE_EXPIRE_TIME):
|
to_return = False
|
||||||
print("Re-filling cache!")
|
if(t - self.lastCacheUpdate > CACHE_EXPIRE_TIME and not self.updating):
|
||||||
|
to_return = True
|
||||||
await self.fill_cache()
|
await self.fill_cache()
|
||||||
self.lastCacheUpdate = t
|
return to_return
|
||||||
|
|
||||||
def add_cache(self, order):
|
def add_cache(self, order):
|
||||||
self.cache[order.code] = order
|
self.cache[order.code] = order
|
||||||
|
@ -270,25 +283,44 @@ class OrderManager:
|
||||||
self.order_list.remove(code)
|
self.order_list.remove(code)
|
||||||
|
|
||||||
async def fill_cache(self):
|
async def fill_cache(self):
|
||||||
await loadItems()
|
# Check cache lock
|
||||||
await loadQuestions()
|
if self.updating == True: return
|
||||||
|
# Set cache lock
|
||||||
|
self.updating = True
|
||||||
|
start_time = time()
|
||||||
|
logger.info("[CACHE] Filling cache...")
|
||||||
|
# Index item's ids
|
||||||
|
await load_items()
|
||||||
|
|
||||||
|
# Index questions' types
|
||||||
|
await load_questions()
|
||||||
|
|
||||||
|
# Clear cache data completely
|
||||||
self.empty()
|
self.empty()
|
||||||
p = 0
|
p = 0
|
||||||
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
while 1:
|
while 1:
|
||||||
p += 1
|
p += 1
|
||||||
res = await client.get(join(base_url_event, f"orders/?page={p}"), headers=headers)
|
res = await client.get(join(base_url_event, f"orders/?page={p}"), headers=headers)
|
||||||
|
if res.status_code == 404: break
|
||||||
if res.status_code == 404: break
|
# Parse order data
|
||||||
|
data = res.json()
|
||||||
data = res.json()
|
for o in data['results']:
|
||||||
for o in data['results']:
|
o = Order(o)
|
||||||
o = Order(o)
|
if o.status in ['canceled', 'expired']:
|
||||||
if o.status in ['canceled', 'expired']:
|
self.remove_cache(o.code)
|
||||||
self.remove_cache(o.code)
|
else:
|
||||||
else:
|
self.add_cache(Order(o))
|
||||||
self.add_cache(Order(o))
|
self.lastCacheUpdate = time()
|
||||||
|
logger.info(f"[CACHE] Cache filled in {self.lastCacheUpdate - start_time}s.")
|
||||||
|
except Exception as ex:
|
||||||
|
logger.error("[CACHE] Error while refreshing cache.", ex)
|
||||||
|
finally:
|
||||||
|
self.updating = False
|
||||||
|
# Validating rooms
|
||||||
|
rooms = list(filter(lambda o: (o.code == o.room_id), self.cache.values()))
|
||||||
|
asyncio.create_task(validate_rooms(None, rooms, self))
|
||||||
|
|
||||||
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):
|
||||||
|
|
||||||
|
@ -298,7 +330,7 @@ class OrderManager:
|
||||||
if order.nfc_id == nfc_id:
|
if order.nfc_id == nfc_id:
|
||||||
return order
|
return order
|
||||||
|
|
||||||
await self.updateCache()
|
await self.update_cache()
|
||||||
# If a cached order is needed, just get it if available
|
# If a cached order is needed, just get it if available
|
||||||
if code and cached and code in self.cache and time()-self.cache[code].time < 3600:
|
if code and cached and code in self.cache and time()-self.cache[code].time < 3600:
|
||||||
return self.cache[code]
|
return self.cache[code]
|
||||||
|
@ -309,7 +341,7 @@ class OrderManager:
|
||||||
secret = request.cookies.get("foxo_secret")
|
secret = request.cookies.get("foxo_secret")
|
||||||
|
|
||||||
if re.match('^[A-Z0-9]{5}$', code or '') and (secret is None or re.match('^[a-z0-9]{16,}$', secret)):
|
if re.match('^[A-Z0-9]{5}$', code or '') and (secret is None or re.match('^[a-z0-9]{16,}$', secret)):
|
||||||
print('Fetching', code, 'with secret', secret)
|
if DEV_MODE and EXTRA_PRINTS: logger.debug(f'Fetching {code} with secret {secret}')
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
res = await client.get(join(base_url_event, f"orders/{code}/"), headers=headers)
|
res = await client.get(join(base_url_event, f"orders/{code}/"), headers=headers)
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
from config import *
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
from sanic import Blueprint, exceptions
|
||||||
|
from sanic.log import logger
|
||||||
|
|
||||||
|
jobs = []
|
||||||
|
|
||||||
|
def draw_profile (source, member, position, font, size=(170, 170), border_width=5):
|
||||||
|
idraw = ImageDraw.Draw(source)
|
||||||
|
source_size = source.size
|
||||||
|
main_fill = (187, 198, 206)
|
||||||
|
propic_x = position[0]
|
||||||
|
propic_y = (source_size[1] // 2) - (size[1] // 2)
|
||||||
|
border_loc = (propic_x, propic_y, propic_x + size[0] + border_width * 2, propic_y + size[1] + border_width *2)
|
||||||
|
profile_location = (propic_x + border_width, propic_y + border_width)
|
||||||
|
propic_name_y = propic_y + size[1] + border_width + 20
|
||||||
|
border_color = SPONSORSHIP_COLOR_MAP[member['sponsorship']] if member['sponsorship'] in SPONSORSHIP_COLOR_MAP.keys() else (84, 110, 122)
|
||||||
|
# Draw border
|
||||||
|
idraw.rounded_rectangle(border_loc, border_width, border_color)
|
||||||
|
# Draw profile picture
|
||||||
|
with Image.open(f'res/propic/{member['propic'] or 'default.png'}') as to_add:
|
||||||
|
source.paste(to_add.resize (size), profile_location)
|
||||||
|
name_len = idraw.textlength(str(member['name']), font)
|
||||||
|
calc_size = 0
|
||||||
|
if name_len > size[0]:
|
||||||
|
calc_size = size[0] * 20 / name_len if name_len > size[0] else 20
|
||||||
|
font = ImageFont.truetype(font.path, calc_size)
|
||||||
|
name_len = idraw.textlength(str(member['name']), font)
|
||||||
|
name_loc = (position[0] + ((size[0] / 2) - name_len / 2), propic_name_y + (calc_size/2))
|
||||||
|
name_color = SPONSORSHIP_COLOR_MAP[member['sponsorship']] if member['sponsorship'] in SPONSORSHIP_COLOR_MAP.keys() else main_fill
|
||||||
|
idraw.text(name_loc, str(member['name']), font=font, fill=name_color)
|
||||||
|
|
||||||
|
async def generate_room_preview(request, code, room_data):
|
||||||
|
if code in jobs: raise exceptions.SanicException("Please try again later!", status_code=409)
|
||||||
|
font_path = f'res/font/NotoSans-Bold.ttf'
|
||||||
|
main_fill = (187, 198, 206)
|
||||||
|
propic_size = (170, 170)
|
||||||
|
logo_size = (200, 43)
|
||||||
|
border_width = 5
|
||||||
|
propic_gap = 50
|
||||||
|
propic_width = propic_size[0] + (border_width * 2)
|
||||||
|
propic_total_width = propic_width + propic_gap
|
||||||
|
jobs.append(code)
|
||||||
|
try:
|
||||||
|
room_data = await get_room(request, code) if not room_data else room_data
|
||||||
|
if not room_data: return
|
||||||
|
width = max([(propic_width + propic_gap) * int(room_data['capacity']) + propic_gap, 670])
|
||||||
|
height = int(width * 0.525)
|
||||||
|
font = ImageFont.truetype(font_path, 20)
|
||||||
|
|
||||||
|
# Recalculate gap
|
||||||
|
propic_gap = (width - (propic_width * int(room_data['capacity']))) // (int(room_data['capacity']) + 1)
|
||||||
|
propic_total_width = propic_width + propic_gap
|
||||||
|
|
||||||
|
# Define output image
|
||||||
|
with Image.new('RGB', (width, height), (17, 25, 31)) as source:
|
||||||
|
# Draw logo
|
||||||
|
with (Image.open('res/furizon.png') as logo, logo.resize(logo_size).convert('RGBA') as resized_logo):
|
||||||
|
source.paste(resized_logo, ((source.size[0] // 2) - (logo_size[0] // 2), 10), resized_logo)
|
||||||
|
i_draw = ImageDraw.Draw(source)
|
||||||
|
# Draw room's name
|
||||||
|
room_name_len = i_draw.textlength(room_data['name'], font)
|
||||||
|
i_draw.text((((width / 2) - room_name_len / 2), 55), room_data['name'], font=font, fill=main_fill)
|
||||||
|
# Draw members
|
||||||
|
for m in range (room_data['capacity']):
|
||||||
|
member = room_data['members'][m] if m < len(room_data['members']) else { 'name': 'Empty', 'propic': '../new.png', 'sponsorship': None }
|
||||||
|
font = ImageFont.truetype(font_path, 20)
|
||||||
|
draw_profile(source, member, (propic_gap + (propic_total_width * m), 63), font, propic_size, border_width)
|
||||||
|
source.save(f'res/rooms/{code}.jpg', 'JPEG', quality=60)
|
||||||
|
except Exception as err:
|
||||||
|
if EXTRA_PRINTS: logger.exception(str(err))
|
||||||
|
finally:
|
||||||
|
# Remove fault job
|
||||||
|
if len(jobs) > 0: jobs.pop()
|
||||||
|
if not room_data:
|
||||||
|
raise exceptions.SanicException("There's no room with that code.", status_code=404)
|
||||||
|
|
||||||
|
async def get_room (request, code):
|
||||||
|
order_data = await request.app.ctx.om.get_order(code=code)
|
||||||
|
if not order_data or not order_data.room_owner: return None
|
||||||
|
members_map = [{'name': order_data.name, 'propic': order_data.propic, 'sponsorship': order_data.sponsorship}]
|
||||||
|
for member_code in order_data.room_members:
|
||||||
|
if member_code == order_data.code: continue
|
||||||
|
member_order = await request.app.ctx.om.get_order(code=member_code)
|
||||||
|
if not member_order: continue
|
||||||
|
members_map.append ({'name': member_order.name, 'propic': member_order.propic, 'sponsorship': member_order.sponsorship})
|
||||||
|
return {'name': order_data.room_name,
|
||||||
|
'confirmed': order_data.room_confirmed,
|
||||||
|
'capacity': order_data.room_person_no,
|
||||||
|
'free_spots': order_data.room_person_no - len(members_map),
|
||||||
|
'members': members_map}
|
|
@ -0,0 +1,24 @@
|
||||||
|
ROOM_ERROR_TYPES = {
|
||||||
|
'room_id_mismatch': "There's a member in your room that is actually in another room, too. Please contact us as soon as possible in order to fix this issue.",
|
||||||
|
'unpaid': "Somebody in your room has not paid for their reservation, yet.",
|
||||||
|
'type_mismatch': "A member in your room has a ticket for a different type of room capacity. This happens when users swap their room types with others, without abandoning the room.",
|
||||||
|
'daily': "Some member in your room has a Daily ticket. These tickets do not include a hotel reservation.",
|
||||||
|
'capacity_mismatch': "The number of people in your room mismatches your type of ticket."
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOM_UNCONFIRM_TITLE = "Your room got unconfirmed"
|
||||||
|
ROOM_UNCONFIRM_TEXT = {
|
||||||
|
'html': "Hello <b>{0}</b><br>We had to <b>unconfirm</b> your room <i>'{1}'</i> due to the following issues:<br></p>{2}<br><p>Please contact your room's owner or contact our support for further informations at <a href=\"https://furizon.net/contact/\"> https://furizon.net/contact/</a>.<br>Thank you.<br><br><a class=\"link\" style=\"background-color: #1095c1; color: #fff;\" href=\"https://reg.furizon.net/manage/welcome\">Manage booking</a>",
|
||||||
|
'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"
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCALES = {
|
||||||
|
'shuttle_link': {
|
||||||
|
'en': 'Book now',
|
||||||
|
'it': 'Prenota ora'
|
||||||
|
},
|
||||||
|
'shuttle_link_url': {
|
||||||
|
'en': 'https://visitfiemme.regiondo.com/furizon?_ga=2.129644046.307369854.1705325023-235291123.1705325023',
|
||||||
|
'it': 'https://experience.visitfiemme.it/furizon'
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ bp = Blueprint("propic", url_prefix="/manage/propic")
|
||||||
async def resetDefaultPropic(request, order: Order, isFursuiter, sendAnswer=True):
|
async def resetDefaultPropic(request, order: Order, isFursuiter, sendAnswer=True):
|
||||||
s = "_fursuiter" if isFursuiter else ""
|
s = "_fursuiter" if isFursuiter else ""
|
||||||
if (EXTRA_PRINTS):
|
if (EXTRA_PRINTS):
|
||||||
print("Resetting {fn} picture for {orderCod}".format(fn="Badge" if not isFursuiter else "fursuit", orderCod = order.code))
|
logger.info("Resetting {fn} picture for {orderCod}".format(fn="Badge" if not isFursuiter else "fursuit", orderCod = order.code))
|
||||||
with open("res/propic/default.png", "rb") as f:
|
with open("res/propic/default.png", "rb") as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
f.close()
|
f.close()
|
||||||
|
@ -56,7 +56,7 @@ async def upload_propic(request, order: Order):
|
||||||
|
|
||||||
# Check max file size
|
# Check max file size
|
||||||
if EXTRA_PRINTS:
|
if EXTRA_PRINTS:
|
||||||
print(f"Image {fn} weight: {len(body[0].body)} bytes")
|
logger.debug (f"Image {fn} weight: {len(body[0].body)} bytes")
|
||||||
if len(body[0].body) > PROPIC_MAX_FILE_SIZE:
|
if len(body[0].body) > PROPIC_MAX_FILE_SIZE:
|
||||||
raise exceptions.BadRequest("File size too large for " + ("Profile picture" if fn == 'propic' else 'Fursuit picture'))
|
raise exceptions.BadRequest("File size too large for " + ("Profile picture" if fn == 'propic' else 'Fursuit picture'))
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18,11H6V6H18M16.5,17A1.5,1.5 0 0,1 15,15.5A1.5,1.5 0 0,1 16.5,14A1.5,1.5 0 0,1 18,15.5A1.5,1.5 0 0,1 16.5,17M7.5,17A1.5,1.5 0 0,1 6,15.5A1.5,1.5 0 0,1 7.5,14A1.5,1.5 0 0,1 9,15.5A1.5,1.5 0 0,1 7.5,17M4,16C4,16.88 4.39,17.67 5,18.22V20A1,1 0 0,0 6,21H7A1,1 0 0,0 8,20V19H16V20A1,1 0 0,0 17,21H18A1,1 0 0,0 19,20V18.22C19.61,17.67 20,16.88 20,16V6C20,2.5 16.42,2 12,2C7.58,2 4,2.5 4,6V16Z" /></svg>
|
After Width: | Height: | Size: 466 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18,16.08C17.24,16.08 16.56,16.38 16.04,16.85L8.91,12.7C8.96,12.47 9,12.24 9,12C9,11.76 8.96,11.53 8.91,11.3L15.96,7.19C16.5,7.69 17.21,8 18,8A3,3 0 0,0 21,5A3,3 0 0,0 18,2A3,3 0 0,0 15,5C15,5.24 15.04,5.47 15.09,5.7L8.04,9.81C7.5,9.31 6.79,9 6,9A3,3 0 0,0 3,12A3,3 0 0,0 6,15C6.79,15 7.5,14.69 8.04,14.19L15.16,18.34C15.11,18.55 15.08,18.77 15.08,19C15.08,20.61 16.39,21.91 18,21.91C19.61,21.91 20.92,20.61 20.92,19A2.92,2.92 0 0,0 18,16.08Z" /></svg>
|
After Width: | Height: | Size: 521 B |
|
@ -1,4 +1,5 @@
|
||||||
div.room-actions {
|
div.room-actions {
|
||||||
|
container-name: room-actions;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
room.py
34
room.py
|
@ -3,6 +3,8 @@ from sanic import Blueprint, exceptions
|
||||||
from random import choice
|
from random import choice
|
||||||
from ext import *
|
from ext import *
|
||||||
from config import headers
|
from config import headers
|
||||||
|
import os
|
||||||
|
from image_util import generate_room_preview, get_room
|
||||||
|
|
||||||
bp = Blueprint("room", url_prefix="/manage/room")
|
bp = Blueprint("room", url_prefix="/manage/room")
|
||||||
|
|
||||||
|
@ -63,6 +65,7 @@ async def delete_room(request, order: Order):
|
||||||
await order.edit_answer('room_members', None)
|
await order.edit_answer('room_members', None)
|
||||||
await order.edit_answer('room_secret', None)
|
await order.edit_answer('room_secret', None)
|
||||||
await order.send_answers()
|
await order.send_answers()
|
||||||
|
remove_room_preview (order.code)
|
||||||
return redirect('/manage/welcome')
|
return redirect('/manage/welcome')
|
||||||
|
|
||||||
@bp.post("/join")
|
@bp.post("/join")
|
||||||
|
@ -112,7 +115,7 @@ async def join_room(request, order: Order):
|
||||||
|
|
||||||
await room_owner.edit_answer('pending_roommates', ','.join(pending_roommates))
|
await room_owner.edit_answer('pending_roommates', ','.join(pending_roommates))
|
||||||
await room_owner.send_answers()
|
await room_owner.send_answers()
|
||||||
|
remove_room_preview (code)
|
||||||
return redirect('/manage/welcome')
|
return redirect('/manage/welcome')
|
||||||
|
|
||||||
@bp.route("/kick/<code>")
|
@bp.route("/kick/<code>")
|
||||||
|
@ -135,7 +138,7 @@ async def kick_member(request, code, order: Order):
|
||||||
|
|
||||||
await order.send_answers()
|
await order.send_answers()
|
||||||
await to_kick.send_answers()
|
await to_kick.send_answers()
|
||||||
|
remove_room_preview (order.code)
|
||||||
return redirect('/manage/welcome')
|
return redirect('/manage/welcome')
|
||||||
|
|
||||||
@bp.route("/renew_secret")
|
@bp.route("/renew_secret")
|
||||||
|
@ -202,7 +205,7 @@ async def approve_roomreq(request, code, order: Order):
|
||||||
|
|
||||||
await pending_member.send_answers()
|
await pending_member.send_answers()
|
||||||
await order.send_answers()
|
await order.send_answers()
|
||||||
|
remove_room_preview(order.code)
|
||||||
return redirect('/manage/welcome')
|
return redirect('/manage/welcome')
|
||||||
|
|
||||||
@bp.route("/leave")
|
@bp.route("/leave")
|
||||||
|
@ -226,7 +229,7 @@ async def leave_room(request, order: Order):
|
||||||
|
|
||||||
await room_owner.send_answers()
|
await room_owner.send_answers()
|
||||||
await order.send_answers()
|
await order.send_answers()
|
||||||
|
remove_room_preview (order.room_id)
|
||||||
return redirect('/manage/welcome')
|
return redirect('/manage/welcome')
|
||||||
|
|
||||||
@bp.route("/reject/<code>")
|
@bp.route("/reject/<code>")
|
||||||
|
@ -282,7 +285,7 @@ async def rename_room(request, order: Order):
|
||||||
|
|
||||||
await order.edit_answer("room_name", name)
|
await order.edit_answer("room_name", name)
|
||||||
await order.send_answers()
|
await order.send_answers()
|
||||||
|
remove_room_preview(order.code)
|
||||||
return redirect('/manage/welcome')
|
return redirect('/manage/welcome')
|
||||||
|
|
||||||
@bp.route("/confirm")
|
@bp.route("/confirm")
|
||||||
|
@ -350,3 +353,24 @@ async def confirm_room(request, order: Order, quotas: Quotas):
|
||||||
await rm.send_answers()
|
await rm.send_answers()
|
||||||
|
|
||||||
return redirect('/manage/welcome')
|
return redirect('/manage/welcome')
|
||||||
|
|
||||||
|
async def get_room_with_order (request, code):
|
||||||
|
order_data = await request.app.ctx.om.get_order(code=code)
|
||||||
|
if not order_data or not order_data.room_owner: return None
|
||||||
|
|
||||||
|
def remove_room_preview(code):
|
||||||
|
preview_file = f"res/rooms/{code}.jpg"
|
||||||
|
try:
|
||||||
|
if os.path.exists(preview_file): os.remove(preview_file)
|
||||||
|
except Exception as ex:
|
||||||
|
if (EXTRA_PRINTS): logger.exception(str(ex))
|
||||||
|
|
||||||
|
@bp.route("/view/<code>")
|
||||||
|
async def get_view(request, code):
|
||||||
|
room_file_name = f"res/rooms/{code}.jpg"
|
||||||
|
room_data = await get_room(request, code)
|
||||||
|
if not room_data: raise exceptions.NotFound("No room was found with that code.")
|
||||||
|
if not os.path.exists(room_file_name):
|
||||||
|
await generate_room_preview(request, code, room_data)
|
||||||
|
tpl = request.app.ctx.tpl.get_template('view_room.html')
|
||||||
|
return html(tpl.render(preview=room_file_name, room_data=room_data))
|
6
stats.py
6
stats.py
|
@ -6,7 +6,7 @@ bp = Blueprint("stats", url_prefix="/manage")
|
||||||
|
|
||||||
@bp.route("/sponsorcount")
|
@bp.route("/sponsorcount")
|
||||||
async def sponsor_count(request, order: Order):
|
async def sponsor_count(request, order: Order):
|
||||||
await request.app.ctx.om.updateCache()
|
await request.app.ctx.om.update_cache()
|
||||||
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: x[1].ans('fursona_name')) if value.status not in ['c', 'e']}
|
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: x[1].ans('fursona_name')) if value.status not in ['c', 'e']}
|
||||||
|
|
||||||
tpl = request.app.ctx.tpl.get_template('sponsorcount.html')
|
tpl = request.app.ctx.tpl.get_template('sponsorcount.html')
|
||||||
|
@ -14,7 +14,7 @@ async def sponsor_count(request, order: Order):
|
||||||
|
|
||||||
@bp.route("/nosecount")
|
@bp.route("/nosecount")
|
||||||
async def nose_count(request, order: Order):
|
async def nose_count(request, order: Order):
|
||||||
await request.app.ctx.om.updateCache()
|
await request.app.ctx.om.update_cache()
|
||||||
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']}
|
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']}
|
||||||
|
|
||||||
tpl = request.app.ctx.tpl.get_template('nosecount.html')
|
tpl = request.app.ctx.tpl.get_template('nosecount.html')
|
||||||
|
@ -22,7 +22,7 @@ async def nose_count(request, order: Order):
|
||||||
|
|
||||||
@bp.route("/fursuitcount")
|
@bp.route("/fursuitcount")
|
||||||
async def fursuit_count(request, order: Order):
|
async def fursuit_count(request, order: Order):
|
||||||
await request.app.ctx.om.updateCache()
|
await request.app.ctx.om.update_cache()
|
||||||
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: x[1].ans('fursona_name')) if value.status not in ['c', 'e']}
|
orders = {key:value for key,value in sorted(request.app.ctx.om.cache.items(), key=lambda x: x[1].ans('fursona_name')) if value.status not in ['c', 'e']}
|
||||||
|
|
||||||
tpl = request.app.ctx.tpl.get_template('fursuitcount.html')
|
tpl = request.app.ctx.tpl.get_template('fursuitcount.html')
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<h2>Admin panel</h2>
|
<h2>Admin panel</h2>
|
||||||
<a href="/manage/admin/cache/clear" role="button" title="Reload the orders' data and the re-sync items' indexes from pretix">Clear cache</a>
|
<a href="/manage/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/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>
|
||||||
<hr>
|
<hr>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
<meta name="viewport" content="width=400rem" />
|
<meta name="viewport" content="width=400rem" />
|
||||||
|
|
|
@ -84,6 +84,8 @@
|
||||||
<a style="grid-area: 1 / 1 / 2 / 2;" role="button" href="javascript:document.getElementById('modal-roomrename').setAttribute('open', 'true');">Rename room</a>
|
<a style="grid-area: 1 / 1 / 2 / 2;" role="button" href="javascript:document.getElementById('modal-roomrename').setAttribute('open', 'true');">Rename room</a>
|
||||||
<a style="grid-area: 1 / 2 / 2 / 3;" href="/manage/room/delete" role="button" {{'disabled' if (len(room_members) > 1) else ''}} >Delete room</a>
|
<a style="grid-area: 1 / 2 / 2 / 3;" href="/manage/room/delete" role="button" {{'disabled' if (len(room_members) > 1) else ''}} >Delete room</a>
|
||||||
<a style="grid-area: 2 / 1 / 3 / 3; display:block;" role="button" {% if not room.forbidden and len(room_members) == order.room_person_no %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}}</strong> room</a>
|
<a style="grid-area: 2 / 1 / 3 / 3; display:block;" role="button" {% if not room.forbidden and len(room_members) == order.room_person_no %}href="javascript:document.getElementById('modal-roomconfirm').setAttribute('open', 'true');"{% endif %}>Confirm <strong>{{[None,'single','double','triple','quadruple','quintuple'][order.room_person_no]}}</strong> room</a>
|
||||||
|
{% else %}
|
||||||
|
{# <a style="grid-area: 1 / 1 / 2 / 2;" role="button" href="javascript:navigator.share({title: 'Furizon room', text:'Viewing room {{order.room_name}}', url: `${window.location.protocol}//${window.location.host}/manage/room/view/{{order.code}}}`});">Share</a> #}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if order.room_id and not order.room_confirmed %}
|
{% if order.room_id and not order.room_confirmed %}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!--By Drew tha woof-->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="supported-color-schemes" content="light dark">
|
||||||
|
<style media="all" type="text/css">
|
||||||
|
* { color: #bbc6ce; }
|
||||||
|
.body { width: 100%; margin: 0; background-color: #11191f; }
|
||||||
|
.container { max-width: 40em; padding: 1em; margin: 0 auto; }
|
||||||
|
.title { font-size: 1.75em; margin-bottom: 1.2em; color: #e1e6eb; margin-top: 0; font-family: sans-serif; }
|
||||||
|
.main-content { margin-top: 0; font-style: normal; font-weight: 400; font-family: sans-serif;}
|
||||||
|
.con-logo { height:2em;}
|
||||||
|
.link { text-decoration: none; background-color: #1095c1; color: #fff; padding: 1em; border-radius: 5px; font-weight: 600; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<div class="body">
|
||||||
|
<div class="container">
|
||||||
|
<img src="https://reg.furizon.net/res/furizon.png" class="con-logo">
|
||||||
|
<div>
|
||||||
|
<h2 class="title">{{title}}</h2>
|
||||||
|
<p class="main-content">{{body}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</html>
|
|
@ -1,5 +1,13 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Furizon 2024 Nosecount{% endblock %}
|
{% block title %}Furizon 2024 Nosecount{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
<meta property="og:title" content="Nose count - Furizon" />
|
||||||
|
<meta property="og:image:type" content="image/jpeg" />
|
||||||
|
<meta property="og:image:alt" content="Furizon logo" />
|
||||||
|
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
|
||||||
|
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
|
||||||
|
<meta property="og:description" content="Explore this year's rooms, find your friends and plan your meet-ups."/>
|
||||||
|
{% endblock %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<main class="container">
|
<main class="container">
|
||||||
{% if order and order.isAdmin() %}
|
{% if order and order.isAdmin() %}
|
||||||
|
@ -12,8 +20,11 @@
|
||||||
</picture>
|
</picture>
|
||||||
</header>
|
</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>
|
<p>Welcome to the nosecount page! Here you can see all of the available rooms at the convention, as well as the occupants currently staying in each room. Use this page to find your friends and plan your meet-ups.</p>
|
||||||
{% for o in orders.values() %}
|
{% for o in orders.values() if (o.code == o.room_id and o.room_confirmed) %}
|
||||||
{% if o.code == o.room_id and o.room_confirmed %}
|
{% if loop.first %}
|
||||||
|
<hr />
|
||||||
|
<h1>Confirmed rooms{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
|
||||||
|
{% endif %}
|
||||||
<h4 style="margin-top:1em;">
|
<h4 style="margin-top:1em;">
|
||||||
<span>{{o.room_name}}</span>
|
<span>{{o.room_name}}</span>
|
||||||
{% if order and order.isAdmin() %}
|
{% if order and order.isAdmin() %}
|
||||||
|
@ -38,15 +49,14 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed) %}
|
{% for o in orders.values() if (o.code == o.room_id and not o.room_confirmed) %}
|
||||||
{% if loop.first %}
|
{% if loop.first %}
|
||||||
<hr />
|
<hr />
|
||||||
<h1>Unconfirmed rooms</h1>
|
<h1>Unconfirmed rooms{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
|
||||||
<p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p>
|
<p>These unconfirmed rooms are still being organized and may be subject to change. These rooms may also have openings for additional roommates. If you are interested in sharing a room, you can use this page to find potential roommates</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h3>
|
<h4>
|
||||||
<span>{{o.room_name}}</span>
|
<span>{{o.room_name}}</span>
|
||||||
{% if o.room_person_no - len(o.room_members) > 0 %} <span class="nsc-room-counter"> - Remaining slots: {{o.room_person_no - len(o.room_members)}}</span> {% endif %}
|
{% if o.room_person_no - len(o.room_members) > 0 %} <span class="nsc-room-counter"> - Remaining slots: {{o.room_person_no - len(o.room_members)}}</span> {% endif %}
|
||||||
{% if order and order.isAdmin() %}
|
{% if order and order.isAdmin() %}
|
||||||
|
@ -55,7 +65,7 @@
|
||||||
<a class="act-del" onclick="confirmAction('delete', this)" action="/manage/admin/room/delete/{{o.code}}"><img src="/res/icons/delete.svg" class="icon" /><span>Delete</span></a>
|
<a class="act-del" onclick="confirmAction('delete', this)" action="/manage/admin/room/delete/{{o.code}}"><img src="/res/icons/delete.svg" class="icon" /><span>Delete</span></a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h3>
|
</h4>
|
||||||
<div class="grid people" style="padding-bottom:1em;">
|
<div class="grid people" style="padding-bottom:1em;">
|
||||||
{% for m in o.room_members %}
|
{% for m in o.room_members %}
|
||||||
{% if m in orders %}
|
{% if m in orders %}
|
||||||
|
@ -76,7 +86,7 @@
|
||||||
{% for person in orders.values() if not person.room_id and (not person.room_confirmed) and not person.daily %}
|
{% for person in orders.values() if not person.room_id and (not person.room_confirmed) and not person.daily %}
|
||||||
{% if loop.first %}
|
{% if loop.first %}
|
||||||
<hr />
|
<hr />
|
||||||
<h1>Roomless furs</h1>
|
<h1>Roomless furs{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
|
||||||
<p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p>
|
<p>These furs have not yet secured a room for the convention. If you see your name on this list, please make sure to secure a room before the deadline to avoid being placed in a random room. If you are looking for a roommate or have an open spot in your room, you can use this page to find and connect with other furries who are also looking for housing 🎲</p>
|
||||||
<div class="grid people" style="padding-bottom:1em;">
|
<div class="grid people" style="padding-bottom:1em;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -93,7 +103,7 @@
|
||||||
{% for person in orders.values() if person.daily %}
|
{% for person in orders.values() if person.daily %}
|
||||||
{% if loop.first %}
|
{% if loop.first %}
|
||||||
<hr />
|
<hr />
|
||||||
<h1>Daily furs!</h1>
|
<h1>Daily furs!{% if order and order.isAdmin() %}<span> ({{loop.length}})</span>{% endif %}</h1>
|
||||||
<p>These furs will not stay in our hotels, but may be there with us just a few days!</p>
|
<p>These furs will not stay in our hotels, but may be there with us just a few days!</p>
|
||||||
<div class="grid people" style="padding-bottom:1em;">
|
<div class="grid people" style="padding-bottom:1em;">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Furizon 2024 Sponsorcount{% endblock %}
|
{% block title %}Furizon 2024 Sponsorcount{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
<meta property="og:title" content="Sponsor count - Furizon" />
|
||||||
|
<meta property="og:image:type" content="image/jpeg" />
|
||||||
|
<meta property="og:image:alt" content="Furizon logo" />
|
||||||
|
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
|
||||||
|
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
|
||||||
|
<meta property="og:description" content="Thanks to all the amazing furs who decided to support us this year ❤️"/>
|
||||||
|
{% endblock %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<header>
|
<header>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{room_data['name']}}{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
<!--Open Graph tags here-->
|
||||||
|
<meta property="og:title" content="View room - Furizon" />
|
||||||
|
<meta property="og:image:type" content="image/jpeg" />
|
||||||
|
<meta property="og:image:alt" content="View of a room" />
|
||||||
|
<meta property="og:image" content="http://localhost:8188/{{preview}}" />
|
||||||
|
<meta property="og:image:secure_url" content="http://localhost:8188/{{preview}}" />
|
||||||
|
<meta property="og:image:width" content="{{230 * room_data['capacity'] + 130}}"/>
|
||||||
|
<meta property="og:image:height" content="270"/>
|
||||||
|
<meta property="og:description" content="Room {{room_data['name']}} has {{'been confirmed.' if room_data['confirmed'] else ('been filled.' if room_data['free_spots'] == 0 else (room_data['free_spots'] | string) + ' free spots out of ' + (room_data['capacity'] | string )) }}"/>
|
||||||
|
{% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<main class="container">
|
||||||
|
<header>
|
||||||
|
<picture>
|
||||||
|
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
|
||||||
|
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;" onload="window.location.href = '/manage/nosecount/'">
|
||||||
|
</picture>
|
||||||
|
</header>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -1,6 +1,16 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{order.name}}'s Booking{% endblock %}
|
{% block title %}{{order.name}}'s Booking{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
<!--Open Graph tags here-->
|
||||||
|
<meta property="og:title" content="Furizon booking management" />
|
||||||
|
<meta property="og:image:type" content="image/png" />
|
||||||
|
<meta property="og:image:alt" content="Furizon's logo" />
|
||||||
|
<meta property="og:image" content="https://reg.furizon.net/res/furizon.png" />
|
||||||
|
<meta property="og:image:secure_url" content="https://reg.furizon.net/res/furizon.png" />
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
{% set locale = order.get_language() %}
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<header>
|
<header>
|
||||||
<picture>
|
<picture>
|
||||||
|
@ -58,6 +68,15 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if order.shuttle_bus %}
|
||||||
|
<tr>
|
||||||
|
<th>Shuttle</th>
|
||||||
|
<td>
|
||||||
|
<img src="/res/icons/bus.svg" class="icon" />
|
||||||
|
{{order.shuttle_bus}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2>Manage your booking</h2>
|
<h2>Manage your booking</h2>
|
||||||
|
@ -67,6 +86,12 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% include 'blocks/badge.html' %}
|
{% include 'blocks/badge.html' %}
|
||||||
|
|
||||||
|
<details id="shuttle">
|
||||||
|
<summary role="button"><img src="/res/icons/bus.svg" class="icon" />Shuttle</summary>
|
||||||
|
<p>This year we teamed up with VisitFiemme to take our guests to the convention.</p>
|
||||||
|
<p style="text-align:right;"><a href="{{LOCALES['shuttle_link_url'][locale]}}" target="_blank" role="button">Book now</a></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details id="barcard">
|
<details id="barcard">
|
||||||
<summary role="button"><img src="/res/icons/bar.svg" class="icon" />Barcard</summary>
|
<summary role="button"><img src="/res/icons/bar.svg" class="icon" />Barcard</summary>
|
||||||
<p>This year's badges will be NFC-enabled and serve as a digital barcard, allowing you to load 'drinks' onto your badge and use it to purchase beverages at the bar without the need for physical cash or the risk of losing a paper barcard. The barcard system will be enabled closer to the convention, so you will have the opportunity to load your badge in advance and enjoy a convenient, cashless experience at the event. Keep an eye out for updates on when the system will be live and available for use.</p>
|
<p>This year's badges will be NFC-enabled and serve as a digital barcard, allowing you to load 'drinks' onto your badge and use it to purchase beverages at the bar without the need for physical cash or the risk of losing a paper barcard. The barcard system will be enabled closer to the convention, so you will have the opportunity to load your badge in advance and enjoy a convenient, cashless experience at the event. Keep an eye out for updates on when the system will be live and available for use.</p>
|
||||||
|
|
168
utils.py
168
utils.py
|
@ -2,6 +2,9 @@ from os.path import join
|
||||||
from sanic import exceptions
|
from sanic import exceptions
|
||||||
from config import *
|
from config import *
|
||||||
import httpx
|
import httpx
|
||||||
|
from messages import ROOM_ERROR_TYPES
|
||||||
|
from email_util import send_unconfirm_message
|
||||||
|
from sanic.log import logger
|
||||||
|
|
||||||
METADATA_TAG = "meta_data"
|
METADATA_TAG = "meta_data"
|
||||||
VARIATIONS_TAG = "variations"
|
VARIATIONS_TAG = "variations"
|
||||||
|
@ -23,7 +26,7 @@ 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 loadQuestions():
|
async def load_questions():
|
||||||
global TYPE_OF_QUESTIONS
|
global TYPE_OF_QUESTIONS
|
||||||
TYPE_OF_QUESTIONS.clear()
|
TYPE_OF_QUESTIONS.clear()
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
@ -38,7 +41,7 @@ async def loadQuestions():
|
||||||
for q in data['results']:
|
for q in data['results']:
|
||||||
TYPE_OF_QUESTIONS[q['id']] = q['type']
|
TYPE_OF_QUESTIONS[q['id']] = q['type']
|
||||||
|
|
||||||
async def loadItems():
|
async def load_items():
|
||||||
global ITEMS_ID_MAP
|
global ITEMS_ID_MAP
|
||||||
global ITEM_VARIATIONS_MAP
|
global ITEM_VARIATIONS_MAP
|
||||||
global CATEGORIES_LIST_MAP
|
global CATEGORIES_LIST_MAP
|
||||||
|
@ -54,14 +57,14 @@ async def loadItems():
|
||||||
data = res.json()
|
data = res.json()
|
||||||
for q in data['results']:
|
for q in data['results']:
|
||||||
# Map item id
|
# Map item id
|
||||||
itemName = checkAndGetName ('item', q)
|
itemName = check_and_get_name ('item', q)
|
||||||
if itemName and itemName in ITEMS_ID_MAP:
|
if itemName and itemName in ITEMS_ID_MAP:
|
||||||
ITEMS_ID_MAP[itemName] = q['id']
|
ITEMS_ID_MAP[itemName] = q['id']
|
||||||
# If item has variations, map them, too
|
# If item has variations, map them, too
|
||||||
if itemName in ITEM_VARIATIONS_MAP and VARIATIONS_TAG in q:
|
if itemName in ITEM_VARIATIONS_MAP and VARIATIONS_TAG in q:
|
||||||
isBedInRoom = itemName == 'bed_in_room'
|
isBedInRoom = itemName == 'bed_in_room'
|
||||||
for v in q[VARIATIONS_TAG]:
|
for v in q[VARIATIONS_TAG]:
|
||||||
variationName = checkAndGetName('variation', v)
|
variationName = check_and_get_name('variation', v)
|
||||||
if variationName and variationName in ITEM_VARIATIONS_MAP[itemName]:
|
if variationName and variationName in ITEM_VARIATIONS_MAP[itemName]:
|
||||||
ITEM_VARIATIONS_MAP[itemName][variationName] = v['id']
|
ITEM_VARIATIONS_MAP[itemName][variationName] = v['id']
|
||||||
if isBedInRoom and variationName in ITEM_VARIATIONS_MAP['bed_in_room']:
|
if isBedInRoom and variationName in ITEM_VARIATIONS_MAP['bed_in_room']:
|
||||||
|
@ -70,52 +73,48 @@ async def loadItems():
|
||||||
roomName = v['value'][list(v['value'].keys())[0]]
|
roomName = v['value'][list(v['value'].keys())[0]]
|
||||||
ROOM_TYPE_NAMES[v['id']] = roomName
|
ROOM_TYPE_NAMES[v['id']] = roomName
|
||||||
# Adds itself to the category list
|
# Adds itself to the category list
|
||||||
categoryName = checkAndGetCategory ('item', q)
|
categoryName = check_and_get_category ('item', q)
|
||||||
if not categoryName: continue
|
if not categoryName: continue
|
||||||
CATEGORIES_LIST_MAP[categoryName].append(q['id'])
|
CATEGORIES_LIST_MAP[categoryName].append(q['id'])
|
||||||
if (EXTRA_PRINTS):
|
if (EXTRA_PRINTS):
|
||||||
print (f'Mapped Items:')
|
logger.debug(f'Mapped Items: %s', ITEMS_ID_MAP)
|
||||||
print (ITEMS_ID_MAP)
|
logger.debug(f'Mapped Variations: %s', ITEM_VARIATIONS_MAP)
|
||||||
print (f'Mapped Variations:')
|
logger.debug(f'Mapped categories: %s', CATEGORIES_LIST_MAP)
|
||||||
print (ITEM_VARIATIONS_MAP)
|
logger.debug(f'Mapped Rooms: %s', ROOM_TYPE_NAMES)
|
||||||
print (f'Mapped categories:')
|
|
||||||
print (CATEGORIES_LIST_MAP)
|
|
||||||
print (f'Mapped Rooms:')
|
|
||||||
print (ROOM_TYPE_NAMES)
|
|
||||||
|
|
||||||
# Tries to get an item name from metadata. Prints a warning if an item has no metadata
|
# Tries to get an item name from metadata. Prints a warning if an item has no metadata
|
||||||
def checkAndGetName(type, q):
|
def check_and_get_name(type, q):
|
||||||
itemName = extractMetadataName(q)
|
itemName = extract_metadata_name(q)
|
||||||
if not itemName and EXTRA_PRINTS:
|
if not itemName and EXTRA_PRINTS:
|
||||||
print (type + ' ' + q['id'] + ' has not been mapped.')
|
logger.warning('%s %s has not been mapped.', type, q['id'])
|
||||||
return itemName
|
return itemName
|
||||||
|
|
||||||
def checkAndGetCategory (type, q):
|
def check_and_get_category (type, q):
|
||||||
categoryName = extractCategory (q)
|
categoryName = extract_category (q)
|
||||||
if not categoryName and EXTRA_PRINTS:
|
if not categoryName and EXTRA_PRINTS:
|
||||||
print (type + ' ' + q['id'] + ' has no category set.')
|
logger.warning('%s %s has no category set.', type, q['id'])
|
||||||
return categoryName
|
return categoryName
|
||||||
|
|
||||||
# Checks if the item has specified metadata name
|
# Checks if the item has specified metadata name
|
||||||
def internalNameCheck (toExtract, name):
|
def internal_name_check (toExtract, name):
|
||||||
return toExtract and name and METADATA_TAG in toExtract and toExtract[METADATA_TAG][METADATA_NAME] == str(name)
|
return toExtract and name and METADATA_TAG in toExtract and toExtract[METADATA_TAG][METADATA_NAME] == str(name)
|
||||||
|
|
||||||
# Returns the item_name metadata from the item or None if not defined
|
# Returns the item_name metadata from the item or None if not defined
|
||||||
def extractMetadataName (toExtract):
|
def extract_metadata_name (toExtract):
|
||||||
return extractData(toExtract, [METADATA_TAG, METADATA_NAME])
|
return extract_data(toExtract, [METADATA_TAG, METADATA_NAME])
|
||||||
|
|
||||||
# Returns the category_name metadata from the item or None if not defined
|
# Returns the category_name metadata from the item or None if not defined
|
||||||
def extractCategory (toExtract):
|
def extract_category (toExtract):
|
||||||
return extractData(toExtract, [METADATA_TAG, METADATA_CATEGORY])
|
return extract_data(toExtract, [METADATA_TAG, METADATA_CATEGORY])
|
||||||
|
|
||||||
def extractData (dataFrom, tags):
|
def extract_data (dataFrom, tags):
|
||||||
data = dataFrom
|
data = dataFrom
|
||||||
for t in tags:
|
for t in tags:
|
||||||
if t not in data: return None
|
if t not in data: return None
|
||||||
data = data[t]
|
data = data[t]
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def keyFromValue(dict, value):
|
def key_from_value(dict, value):
|
||||||
return [k for k,v in dict.items() if v == value]
|
return [k for k,v in dict.items() if v == value]
|
||||||
|
|
||||||
def sizeof_fmt(num, suffix="B"):
|
def sizeof_fmt(num, suffix="B"):
|
||||||
|
@ -125,7 +124,7 @@ def sizeof_fmt(num, suffix="B"):
|
||||||
num /= 1000.0
|
num /= 1000.0
|
||||||
return f"{num:.1f}Yi{suffix}"
|
return f"{num:.1f}Yi{suffix}"
|
||||||
|
|
||||||
async def getOrderByCode(request, code, throwException=False):
|
async def get_order_by_code(request, code, throwException=False):
|
||||||
res = await request.app.ctx.om.get_order(code=code)
|
res = await request.app.ctx.om.get_order(code=code)
|
||||||
if not throwException:
|
if not throwException:
|
||||||
return res
|
return res
|
||||||
|
@ -133,10 +132,111 @@ async def getOrderByCode(request, code, throwException=False):
|
||||||
raise exceptions.BadRequest(f"[getOrderByCode] Code {code} not found!")
|
raise exceptions.BadRequest(f"[getOrderByCode] Code {code} not found!")
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getPeopleInRoomByRoomId(request, roomId):
|
async def get_people_in_room_by_code(request, code, om=None):
|
||||||
c = request.app.ctx.om.cache
|
if not om: om = request.app.ctx.om
|
||||||
ret = []
|
await om.update_cache()
|
||||||
for person in c.values():
|
return filter(lambda rm: rm.room_id == code, om.cache.values())
|
||||||
if person.room_id == roomId:
|
|
||||||
ret.append(person)
|
async def unconfirm_room_by_order(order, room_members:[]=None, throw=True, request=None, om=None):
|
||||||
return ret
|
if not om: om = request.app.ctx.om
|
||||||
|
if not order.room_confirmed:
|
||||||
|
if throw:
|
||||||
|
raise exceptions.BadRequest("Room is not confirmed!")
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
room_members = await get_people_in_room_by_code(request, order.code, om) if not room_members or len(room_members) == 0 else room_members
|
||||||
|
for p in room_members:
|
||||||
|
await p.edit_answer('room_confirmed', "False")
|
||||||
|
await p.send_answers()
|
||||||
|
|
||||||
|
async def validate_rooms(request, rooms, om):
|
||||||
|
logger.info('Validating rooms...')
|
||||||
|
if not om: om = request.app.ctx.om
|
||||||
|
|
||||||
|
failed_rooms = []
|
||||||
|
|
||||||
|
# Validate rooms
|
||||||
|
for order in rooms:
|
||||||
|
# returns tuple (room owner order, check, room error list, room members orders)
|
||||||
|
result = await check_room(request, order, om)
|
||||||
|
order = result[0]
|
||||||
|
check = result[1]
|
||||||
|
if check != None and check == False:
|
||||||
|
failed_rooms.append(result)
|
||||||
|
|
||||||
|
# End here if no room has failed check
|
||||||
|
if len(failed_rooms) == 0:
|
||||||
|
logger.info('[ROOM VALIDATION] Every room passed the check.')
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.warning(f'[ROOM VALIDATION] Room validation failed for orders: %s', list(map(lambda rf: rf[0].code, failed_rooms)))
|
||||||
|
|
||||||
|
# Get confirmed rooms that fail validation
|
||||||
|
failed_confirmed_rooms = list(filter(lambda fr: (fr[0].room_confirmed == True), failed_rooms))
|
||||||
|
|
||||||
|
if len(failed_confirmed_rooms) == 0:
|
||||||
|
logger.info('[ROOM VALIDATION] No rooms to unconfirm.')
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"[ROOM VALIDATION] Trying to unconfirm {len(failed_confirmed_rooms)} rooms...")
|
||||||
|
|
||||||
|
# Try unconfirming them
|
||||||
|
for rtu in failed_confirmed_rooms:
|
||||||
|
order = rtu[0]
|
||||||
|
member_orders = rtu[2]
|
||||||
|
|
||||||
|
# Unconfirm and email users about the room
|
||||||
|
await unconfirm_room_by_order(order, member_orders, False, None, om)
|
||||||
|
|
||||||
|
logger.info(f"[ROOM VALIDATION] Sending unconfirm notice to room members...")
|
||||||
|
sent_count = 0
|
||||||
|
# Send unconfirm notice via email
|
||||||
|
for rtu in failed_confirmed_rooms:
|
||||||
|
order = rtu[0]
|
||||||
|
member_orders = rtu[2]
|
||||||
|
try:
|
||||||
|
await send_unconfirm_message (order, member_orders)
|
||||||
|
sent_count += len(member_orders)
|
||||||
|
except Exception as ex:
|
||||||
|
if EXTRA_PRINTS: logger.exception(str(ex))
|
||||||
|
logger.info(f"[ROOM VALIDATION] Sent {sent_count} emails")
|
||||||
|
|
||||||
|
|
||||||
|
async def check_room(request, order, om=None):
|
||||||
|
room_errors = []
|
||||||
|
room_members = []
|
||||||
|
use_cached = request == None
|
||||||
|
if not om: om = request.app.ctx.om
|
||||||
|
if not order or not order.room_id or order.room_id != order.code: return (order, False, room_members)
|
||||||
|
|
||||||
|
# This is not needed anymore you buy tickets already
|
||||||
|
#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
|
||||||
|
for m in order.room_members:
|
||||||
|
if m == order.code:
|
||||||
|
res = order
|
||||||
|
else:
|
||||||
|
res = await om.get_order(code=m, cached=use_cached)
|
||||||
|
|
||||||
|
# Room user in another room
|
||||||
|
if res.room_id != order.code:
|
||||||
|
room_errors.append('room_id_mismatch')
|
||||||
|
|
||||||
|
if res.status != 'paid':
|
||||||
|
room_errors.append('unpaid')
|
||||||
|
|
||||||
|
if res.bed_in_room != bed_in_room:
|
||||||
|
room_errors.append('type_mismatch')
|
||||||
|
|
||||||
|
if res.daily:
|
||||||
|
room_errors.append('daily')
|
||||||
|
|
||||||
|
room_members.append(res)
|
||||||
|
|
||||||
|
if len(room_members) != order.room_person_no and order.room_person_no != None:
|
||||||
|
room_errors.append('capacity_mismatch')
|
||||||
|
order.set_room_errors(room_errors)
|
||||||
|
return (order, len(room_errors) == 0, room_members)
|
Loading…
Reference in New Issue