diff --git a/admin.py b/admin.py index 45d383a..25362d4 100644 --- a/admin.py +++ b/admin.py @@ -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/') -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/') -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/') -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/') -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: diff --git a/ext.py b/ext.py index 7a48a31..77db90b 100644 --- a/ext.py +++ b/ext.py @@ -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 diff --git a/messages.py b/messages.py new file mode 100644 index 0000000..a9a190a --- /dev/null +++ b/messages.py @@ -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" +} \ No newline at end of file diff --git a/res/font/NotoSans-Bold.ttf b/res/font/NotoSans-Bold.ttf new file mode 100644 index 0000000..d84248e Binary files /dev/null and b/res/font/NotoSans-Bold.ttf differ diff --git a/res/font/pt-serif-caption-latin-400-normal.ttf b/res/font/pt-serif-caption-latin-400-normal.ttf deleted file mode 100644 index 7dcd79e..0000000 Binary files a/res/font/pt-serif-caption-latin-400-normal.ttf and /dev/null differ diff --git a/res/icons/share.svg b/res/icons/share.svg new file mode 100644 index 0000000..99166ee --- /dev/null +++ b/res/icons/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/room.py b/room.py index 4f4917e..2d9a5ee 100644 --- a/room.py +++ b/room.py @@ -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/") @@ -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 ' + 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: diff --git a/tpl/nosecount.html b/tpl/nosecount.html index c6a021b..b9723ec 100644 --- a/tpl/nosecount.html +++ b/tpl/nosecount.html @@ -1,5 +1,13 @@ {% extends "base.html" %} {% block title %}Furizon 2024 Nosecount{% endblock %} +{% block head %} + + + + + + +{% endblock %} {% block main %}
{% if order and order.isAdmin() %} diff --git a/tpl/sponsorcount.html b/tpl/sponsorcount.html index 79fe46e..17807af 100644 --- a/tpl/sponsorcount.html +++ b/tpl/sponsorcount.html @@ -1,5 +1,13 @@ {% extends "base.html" %} {% block title %}Furizon 2024 Sponsorcount{% endblock %} +{% block head %} + + + + + + +{% endblock %} {% block main %}
diff --git a/tpl/view_room.html b/tpl/view_room.html index cefb8ac..f998072 100644 --- a/tpl/view_room.html +++ b/tpl/view_room.html @@ -2,9 +2,14 @@ {% block title %}{{room_data['name']}}{% endblock %} {% block head %} - + + + + + + {% endblock %} {% block main %}
@@ -12,7 +17,7 @@ + >
diff --git a/utils.py b/utils.py index 50427fc..6a1ba1a 100644 --- a/utils.py +++ b/utils.py @@ -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 \ No newline at end of file