[wip4] Room preview generation improvements + Room checking with user email

This commit is contained in:
Andrea Carulli 2024-01-18 18:03:32 +01:00
parent 6fe87b6ea1
commit aa54032492
11 changed files with 201 additions and 73 deletions

View File

@ -1,5 +1,5 @@
from email.mime.text import MIMEText
from sanic import response, redirect, Blueprint, exceptions
from room import unconfirm_room_by_order
from config import *
from utils import *
from ext import *
@ -12,7 +12,7 @@ import json
bp = Blueprint("admin", url_prefix="/manage/admin")
def credentialsCheck(request, order:Order):
def credentials_check(request, order:Order):
if not order:
raise exceptions.Forbidden("You have been logged out. Please access the link in your E-Mail to login again!")
if EXTRA_PRINTS:
@ -22,15 +22,15 @@ def credentialsCheck(request, order:Order):
@bp.get('/cache/clear')
async def clearCache(request, order:Order):
credentialsCheck(request, order)
async def clear_cache(request, order:Order):
credentials_check(request, order)
await request.app.ctx.om.fill_cache()
return redirect(f'/manage/admin')
@bp.get('/loginas/<code>')
async def loginAs(request, code, order:Order):
credentialsCheck(request, order)
dOrder = await getOrderByCode(request, code, throwException=True)
async def login_as(request, code, order:Order):
credentials_check(request, order)
dOrder = await get_order_by_code(request, code, throwException=True)
if(dOrder.isAdmin()):
raise exceptions.Forbidden("You can't login as another admin!")
@ -44,26 +44,18 @@ async def loginAs(request, code, order:Order):
return r
@bp.get('/room/unconfirm/<code>')
async def unconfirmRoom(request, code, order:Order):
credentialsCheck(request, order)
dOrder = await getOrderByCode(request, code, throwException=True)
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()
async def unconfirm_room(request, code, order:Order):
credentials_check(request, order)
dOrder = await get_order_by_code(request, code, throwException=True)
unconfirm_room_by_order(dOrder, True, request)
return redirect(f'/manage/nosecount')
@bp.get('/room/delete/<code>')
async def deleteRoom(request, code, order:Order):
credentialsCheck(request, order)
dOrder = await getOrderByCode(request, code, throwException=True)
async def delete_room(request, code, order:Order):
credentials_check(request, order)
dOrder = await get_order_by_code(request, code, throwException=True)
ppl = getPeopleInRoomByRoomId(request, code)
ppl = get_people_in_room_by_code(request, code)
for p in ppl:
await p.edit_answer('room_id', None)
await p.edit_answer('room_confirmed', "False")
@ -79,9 +71,9 @@ async def deleteRoom(request, code, order:Order):
return redirect(f'/manage/nosecount')
@bp.post('/room/rename/<code>')
async def renameRoom(request, code, order:Order):
credentialsCheck(request, order)
dOrder = await getOrderByCode(request, code, throwException=True)
async def rename_room(request, code, order:Order):
credentials_check(request, order)
dOrder = await get_order_by_code(request, code, throwException=True)
name = request.form.get('name')
if len(name) > 64 or len(name) < 4:

8
ext.py
View File

@ -65,7 +65,7 @@ class Order:
self.has_card = True
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
if p['attendee_name']:
@ -79,7 +79,7 @@ class Order:
self.has_late = True
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
self.bed_in_room = p['variation']
self.room_person_no = ROOM_CAPACITY_MAP[roomTypeId] if roomTypeId in ROOM_CAPACITY_MAP else None
@ -270,8 +270,8 @@ class OrderManager:
self.order_list.remove(code)
async def fill_cache(self):
await loadItems()
await loadQuestions()
await load_items()
await load_questions()
self.empty()
p = 0

6
messages.py Normal file
View File

@ -0,0 +1,6 @@
ROOM_ERROR_MESSAGES = {
'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"
}

BIN
res/font/NotoSans-Bold.ttf Normal file

Binary file not shown.

1
res/icons/share.svg Normal file
View File

@ -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

152
room.py
View File

@ -1,3 +1,6 @@
from email.mime.text import MIMEText
from messages import ROOM_ERROR_MESSAGES
import smtplib
from sanic.response import html, redirect, text
from sanic import Blueprint, exceptions
from random import choice
@ -207,8 +210,8 @@ async def approve_roomreq(request, code, order: Order):
await order.edit_answer('pending_roommates', (','.join([x for x in order.pending_roommates if x != pending_member.code]) or None))
await pending_member.send_answers()
await order.send_answers()
await order.send_answers(order.code)
remove_room_preview()
return redirect('/manage/welcome')
@bp.route("/leave")
@ -232,7 +235,7 @@ async def leave_room(request, order: Order):
await room_owner.send_answers()
await order.send_answers()
remove_room_preview (order.room_id)
return redirect('/manage/welcome')
@bp.route("/reject/<code>")
@ -288,7 +291,7 @@ async def rename_room(request, order: Order):
await order.edit_answer("room_name", name)
await order.send_answers()
remove_room_preview (order.code)
remove_room_preview(order.code)
return redirect('/manage/welcome')
@bp.route("/confirm")
@ -357,6 +360,78 @@ async def confirm_room(request, order: Order, quotas: Quotas):
return redirect('/manage/welcome')
async def unconfirm_room_by_order(order, throw=True, request=None):
if not order.room_confirmed and throw:
raise exceptions.BadRequest("Room is not confirmed!")
ppl = get_people_in_room_by_code(request, order.code)
for p in ppl:
await p.edit_answer('room_confirmed', "False")
await p.send_answers()
async def validate_room(request, order):
check, room_errors, member_orders = await check_room(request, order)
if check == True: return
try:
# Build message
issues_str = ""
for err in room_errors:
if err in ROOM_ERROR_MESSAGES:
issues_str += f" - {ROOM_ERROR_MESSAGES['err']}"
memberMessages = []
for member in member_orders:
msg = MIMEText(f"Hello {member.name}!\n\nWe had to unconfirm your room {order.room_name} due to th{'ese issues' if len(room_errors) > 1 else 'is issue'}:\n{issues_str}\n\nPlease contact your room's owner or contact our support for further informations at https://furizon.net/contact/.\nThank you")
msg['Subject'] = '[Furizon] Your room cannot be confirmed'
msg['From'] = 'Furizon <no-reply@furizon.net>'
msg['To'] = f"{member.name} <{member.email}>"
memberMessages.append(msg)
if (len(memberMessages) == 0): return
s = smtplib.SMTP_SSL(SMTP_HOST)
s.login(SMTP_USER, SMTP_PASSWORD)
for message in memberMessages:
s.sendmail(message['From'], message['to'], message.as_string())
s.quit()
except Exception as ex:
if EXTRA_PRINTS: print(ex)
async def check_room(request, order):
room_errors = []
room_members = []
if not order or not order.room_id or order.room_id != order.code: return False, room_errors, 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 request.app.ctx.om.get_order(code=m)
# 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')
return len(room_errors) == 0, room_errors, room_members
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
@ -369,6 +444,7 @@ async def get_room (request, code):
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}
async def get_room_with_order (request, code):
@ -382,35 +458,67 @@ def remove_room_preview(code):
except Exception as ex:
if (EXTRA_PRINTS): print(ex)
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):
font_path = f'res/font/pt-serif-caption-latin-400-normal.ttf'
main_fill = (16, 149, 193)
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
width = 230 * int(room_data['capacity']) + 130
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)
with Image.new('RGB', (width, 270), (17, 25, 31)) as to_save:
i_draw = ImageDraw.Draw(to_save)
# 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), 10), room_data['name'], font=font, fill=main_fill)
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)
with Image.open(f'res/propic/{member['propic'] or 'default.png'}') as to_add:
to_save.paste(to_add.resize ((180, 180)), (90 + (230 * m), 45))
name_len = i_draw.textlength(str(member['name']), font)
calc_size = 0
if name_len > 180:
calc_size = 180 * 20 / name_len if name_len > 180 else 20
font = ImageFont.truetype(font_path, calc_size)
name_len = i_draw.textlength(str(member['name']), font)
name_loc = ((90 + (230 * m)) + (90 - name_len / 2), 235 + (calc_size/2))
name_color = SPONSORSHIP_COLOR_MAP[member['sponsorship']] if member['sponsorship'] in SPONSORSHIP_COLOR_MAP.keys() else main_fill
i_draw.text(name_loc, str(member['name']), font=font, fill=name_color)
to_save.save(f'res/rooms/{code}.jpg', 'JPEG')
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: print(err)
finally:

View File

@ -1,5 +1,13 @@
{% extends "base.html" %}
{% 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 %}
<main class="container">
{% if order and order.isAdmin() %}

View File

@ -1,5 +1,13 @@
{% extends "base.html" %}
{% 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 %}
<main class="container">
<header>

View File

@ -2,9 +2,14 @@
{% block title %}{{room_data['name']}}{% endblock %}
{% block head %}
<!--Open Graph tags here-->
<meta property="og:title" content="{{room_data['name']}}" />
<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">
@ -12,7 +17,7 @@
<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>

View File

@ -23,7 +23,7 @@ QUESTION_TYPES = { #https://docs.pretix.eu/en/latest/api/resources/questions.htm
TYPE_OF_QUESTIONS = {} # maps questionId -> type
async def loadQuestions():
async def load_questions():
global TYPE_OF_QUESTIONS
TYPE_OF_QUESTIONS.clear()
async with httpx.AsyncClient() as client:
@ -38,7 +38,7 @@ async def loadQuestions():
for q in data['results']:
TYPE_OF_QUESTIONS[q['id']] = q['type']
async def loadItems():
async def load_items():
global ITEMS_ID_MAP
global ITEM_VARIATIONS_MAP
global CATEGORIES_LIST_MAP
@ -54,14 +54,14 @@ async def loadItems():
data = res.json()
for q in data['results']:
# Map item id
itemName = checkAndGetName ('item', q)
itemName = check_and_get_name ('item', q)
if itemName and itemName in ITEMS_ID_MAP:
ITEMS_ID_MAP[itemName] = q['id']
# If item has variations, map them, too
if itemName in ITEM_VARIATIONS_MAP and VARIATIONS_TAG in q:
isBedInRoom = itemName == 'bed_in_room'
for v in q[VARIATIONS_TAG]:
variationName = checkAndGetName('variation', v)
variationName = check_and_get_name('variation', v)
if variationName and variationName in ITEM_VARIATIONS_MAP[itemName]:
ITEM_VARIATIONS_MAP[itemName][variationName] = v['id']
if isBedInRoom and variationName in ITEM_VARIATIONS_MAP['bed_in_room']:
@ -70,7 +70,7 @@ async def loadItems():
roomName = v['value'][list(v['value'].keys())[0]]
ROOM_TYPE_NAMES[v['id']] = roomName
# Adds itself to the category list
categoryName = checkAndGetCategory ('item', q)
categoryName = check_and_get_category ('item', q)
if not categoryName: continue
CATEGORIES_LIST_MAP[categoryName].append(q['id'])
if (EXTRA_PRINTS):
@ -84,38 +84,38 @@ async def loadItems():
print (ROOM_TYPE_NAMES)
# Tries to get an item name from metadata. Prints a warning if an item has no metadata
def checkAndGetName(type, q):
itemName = extractMetadataName(q)
def check_and_get_name(type, q):
itemName = extract_metadata_name(q)
if not itemName and EXTRA_PRINTS:
print (type + ' ' + q['id'] + ' has not been mapped.')
return itemName
def checkAndGetCategory (type, q):
categoryName = extractCategory (q)
def check_and_get_category (type, q):
categoryName = extract_category (q)
if not categoryName and EXTRA_PRINTS:
print (type + ' ' + q['id'] + ' has no category set.')
return categoryName
# 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)
# Returns the item_name metadata from the item or None if not defined
def extractMetadataName (toExtract):
return extractData(toExtract, [METADATA_TAG, METADATA_NAME])
def extract_metadata_name (toExtract):
return extract_data(toExtract, [METADATA_TAG, METADATA_NAME])
# Returns the category_name metadata from the item or None if not defined
def extractCategory (toExtract):
return extractData(toExtract, [METADATA_TAG, METADATA_CATEGORY])
def extract_category (toExtract):
return extract_data(toExtract, [METADATA_TAG, METADATA_CATEGORY])
def extractData (dataFrom, tags):
def extract_data (dataFrom, tags):
data = dataFrom
for t in tags:
if t not in data: return None
data = data[t]
return data
def keyFromValue(dict, value):
def key_from_value(dict, value):
return [k for k,v in dict.items() if v == value]
def sizeof_fmt(num, suffix="B"):
@ -125,7 +125,7 @@ def sizeof_fmt(num, suffix="B"):
num /= 1000.0
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)
if not throwException:
return res
@ -133,10 +133,10 @@ async def getOrderByCode(request, code, throwException=False):
raise exceptions.BadRequest(f"[getOrderByCode] Code {code} not found!")
return res
def getPeopleInRoomByRoomId(request, roomId):
def get_people_in_room_by_code(request, code):
c = request.app.ctx.om.cache
ret = []
for person in c.values():
if person.room_id == roomId:
if person.room_id == code:
ret.append(person)
return ret