We're so back #16
4
app.py
4
app.py
|
@ -139,8 +139,10 @@ async def welcome(request, order: Order, quota: Quotas):
|
|||
else:
|
||||
room_members.append(await app.ctx.om.get_order(code=member_id, cached=True))
|
||||
|
||||
print (order.room_errors)
|
||||
|
||||
tpl = app.ctx.tpl.get_template('welcome.html')
|
||||
return html(tpl.render(order=order, quota=quota, room_members=room_members, pending_roommates=pending_roommates, ROOM_ERROR_MESSAGES=ROOM_ERROR_MESSAGES))
|
||||
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")
|
||||
|
|
|
@ -117,5 +117,6 @@ TG_CHAT_ID = -1234567
|
|||
ADMINS = ['XXXXX', 'YYYYY']
|
||||
|
||||
SMTP_HOST = 'host'
|
||||
SMTP_PORT = 0
|
||||
SMTP_USER = 'user'
|
||||
SMTP_PASSWORD = 'pw'
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
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, SMTP_ADMIN_PASS
|
||||
|
||||
def send_unconfirm_message (room_order, room_errors, orders, title):
|
||||
memberMessages = []
|
||||
|
||||
issues_plain = ""
|
||||
issues_html = "<ul>"
|
||||
|
||||
for err in room_errors:
|
||||
if err in ROOM_ERROR_TYPES.keys():
|
||||
issues_plain += f"{ROOM_ERROR_TYPES[err]}"
|
||||
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 = 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
|
||||
|
||||
def render_email_template(title = "", body = ""):
|
||||
tpl = app.ctx.tpl.get_template('email/comunication.html')
|
||||
return str(tpl.render(title=title, body=body))
|
4
ext.py
4
ext.py
|
@ -98,7 +98,7 @@ class Order:
|
|||
self.payment_provider = data['payment_provider']
|
||||
self.comment = data['comment']
|
||||
self.phone = data['phone']
|
||||
self.room_issues = []
|
||||
self.room_errors = []
|
||||
self.loadAns()
|
||||
def loadAns(self):
|
||||
self.shirt_size = self.ans('shirt_size')
|
||||
|
@ -133,7 +133,7 @@ class Order:
|
|||
return self.data[var]
|
||||
|
||||
def set_room_errors (self, to_set):
|
||||
for s in to_set: self.room_issues.append (s)
|
||||
for s in to_set: self.room_errors.append (s)
|
||||
|
||||
def ans(self, name):
|
||||
for p in self.data['positions']:
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
from config import *
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from sanic import Blueprint, exceptions
|
||||
import textwrap
|
||||
|
||||
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):
|
||||
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: print(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}
|
11
messages.py
11
messages.py
|
@ -1,6 +1,13 @@
|
|||
ROOM_ERROR_MESSAGES = {
|
||||
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"
|
||||
'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 unconfirm your room '{1}' due to the following problem/s:<br>{2}<br>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",
|
||||
'plain': "Hello {0}\nWe had to unconfirm your room '{1}' due to the following problem/s:\n{2}\nPlease contact your room's owner or contact our support for further informations at https://furizon.net/contact/.\nThank you"
|
||||
}
|
90
room.py
90
room.py
|
@ -3,11 +3,8 @@ from sanic import Blueprint, exceptions
|
|||
from random import choice
|
||||
from ext import *
|
||||
from config import headers
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import textwrap
|
||||
import os
|
||||
|
||||
jobs = []
|
||||
from image_util import generate_room_preview, get_room
|
||||
|
||||
bp = Blueprint("room", url_prefix="/manage/room")
|
||||
|
||||
|
@ -357,21 +354,6 @@ async def confirm_room(request, order: Order, quotas: Quotas):
|
|||
|
||||
return redirect('/manage/welcome')
|
||||
|
||||
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}
|
||||
|
||||
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
|
||||
|
@ -383,75 +365,6 @@ 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/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: print(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)
|
||||
|
||||
@bp.route("/view/<code>")
|
||||
async def get_view(request, code):
|
||||
room_file_name = f"res/rooms/{code}.jpg"
|
||||
|
@ -461,4 +374,3 @@ async def get_view(request, code):
|
|||
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))
|
||||
|
|
@ -14,7 +14,8 @@
|
|||
{% if order.room_id and not order.room_confirmed %}
|
||||
<p class="notice">⚠️ Your room hasn't been confirmed yet. Unconfirmed rooms are subject to changes by the staff as we optimize for hotel capacity.</p>
|
||||
{% endif %}
|
||||
{% if order.room_id and len(order.room_issues) > 0 %}
|
||||
{{len(order.room_errors)}}
|
||||
{% if order.room_id and len(order.room_errors) > 0 %}
|
||||
<p class="notice">⚠️ There are some issues with your room:
|
||||
<ul>
|
||||
{% for issue in issues %}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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">
|
||||
<title>Simple Transactional Email</title>
|
||||
<style media="all" type="text/css">
|
||||
:root{
|
||||
--main-background: #fff; --h2-color: #24333e; --color: hsl(205deg, 20%, 32%);
|
||||
--font-family: system-ui,-apple-system,"Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
|
||||
}
|
||||
*, ::after, ::before { box-sizing:border-box; background-repeat:no-repeat; }
|
||||
body { width: 100%; margin: 0; background-color: var(--main-background); }
|
||||
.container { max-width: 40em; padding: 1em; box-sizing: border-box; margin: 0 auto; }
|
||||
h2 { --font-size: 1.75rem; --typography-spacing-vertical: 2.625rem; color: var(--h2-color) }
|
||||
h1, h2, h3, h4, h5, h6 { margin-top: 0; margin-bottom: var(--typography-spacing-vertical); font-size: var(--font-size); font-family: var(--font-family);}
|
||||
address, blockquote, dl, figure, form, ol, p, pre, table, ul {margin-top: 0; color: var(--color); font-style: normal; font-weight: 400; font-family: var(--font-family);}
|
||||
img.con-logo {content: url('data:image/png;base64,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');}
|
||||
|
||||
/* Dark theme */
|
||||
@media only screen and (prefers-color-scheme: dark) {
|
||||
:root{ --main-background: #11191f; --h2-color: #e1e6eb; --color: hsl(205deg, 16%, 77%);}
|
||||
.icon {filter: invert(1);}
|
||||
img.con-logo { content: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKoAAAAlCAYAAADSkHKPAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAALiMAAC4jAXilP3YAAA39SURBVHja7VwJVJXVFrZBK8sGS1FAIU0ya5WK+laDvVf61CzHojRNWTa8UsvM13uvXkWreqmhAhe4DCrO0xWZZZBEZJ5uJCDgBUFUckIU7vSPZ7/9DxdueBkuguO/1/rW+Ydzzn+G7+yzzz7n3h49rpHQNP0MyzPQHhiOZhDJHQEhzJge10FOkpP30Ty9juVpX0LIfT0UuXWko0S1C6x5yvWoC0e4Oc0Di/la6d1blqjsTxxhPmkTXDvvEQDQ67rUhaPnyJqfcMB5Kr17ixKVJvSzN3t9TMQ0COvxtNKzClEVUUQhqiKKKERVRCGqIoooRFVEEWuiMhwdSXP0XHvAsuxEIR9CyOOWZ1dTHiyFu5gPTY+yNy0APMTwzHqsy0Ysj7NYP45+x946tVpXwr7S1vfxm32xDI9ZgPcPdqDMvazS3NHO+8fqSN2Dtz1ROwc6XSaEp+XZ1ZSH5Vl/2afrjR3zCM3T3gIoQg37cwdefgSJM4lm6VUYd1V9ff1DtmYHHHx0V2xiCP5Z/N5UG0S6gyPcIhwgKRiHbZHOgM9iKZaaZouEYn1Z8xRLfCT2/U39QujRWC8/TH+2RTkM2ObZDGdegnneebtq1KPYCPn2gGGZDd1FVDOYXa12u0TNLRAW7zddQSSGGWuLqFiubHvr1FQ3jja1tdOF3xuJ77QdIjvHFFBADW+PqMLgZKVZoSODpxYHwQzFRrUnn2tAVI4wi601JF43IKoQlWaz2bUr7W0hL8z3sjxrrL3CROGYf+B73uocxD4cLOORaHdbgCSahc8PWpWXRq08oTWimsA0GAl9xGq2ysTvfGadp8lkcsHnPhjvYnO+1DcKUW8QouLUGmOl3arwex7Ycb27oy6CLWhF0oArSEqYF4VtWklT0hmo5Ue0aXczzPNY5uMWUmPZnrFFVCRfuRznLBL61dbMBbmM92KbhFnKccuT9abRqM3YXgIlNs8S6PX6prrgdafqIky/SL4seUBokRAPtHjfB8lxSSZxdsvB0ka+jpjuhEzICot9+SeiSiQ9T1GUW8fbi/7Zyvx5SSHqNSCqSTNkMNV4apeQB3U68ZA5afxPDGNo6kRTpQa4yCeSuQjXBDbCNRtDsIZBMwRojE/TRjBGjYKW79sCu8+VZzBf4+nMUuFbxrO5wMaNymDCXVZYgHGmU/Xl34rloxpNDaThUTs1tSsSkZIHwRe2iGprwdae18CiiQW7+pZdYF0ropIkh/tJfF9nEufwChfXz5OP6+fDxzls52P7H+Sj+1Fc9ACKi3HiTGXBkkYsWgl0zGCgzXXivbm+Eu9dgY92Aj7GEcMB4jUX5Qxc9CDxno4dAQxrEolqih6Jz52b4vDRjlK8qIEIZyDRDlJe0Y5SHHynL/xZ/BZVfwzY+BFN6Uh0fwyldPraPDGOIXMxYSOdKC7KkWKjB+3AeNu5GMfvILqfJ8T1G2nc5+QMmn4PXKkBmQ2yVj0ukMqaqEji/W1N9622PYiLOost/6pC1A4QlWHN9XDIZTh7YNB0/oDzWj7JaQ+f6HiGS3JsIInY6baQ5CQBr42VQWJZGktWA5Pg0kRUfdEPwCc62YCUhxAyic80EdWYOFJ6nzCw6Tu8CCer++Z8zPnLBTsPzIZaoFJewHSOVhgowpw0SiATmPRngU0cAnz8gKZ3Yrz4gVbxxbCBSxh4BkMNOeC0WmgTUvqfSWhaWFb5ff+kUVl2Uidt6ruwXPLiiv1ZIWrLBvLqcSfJH/giyXNcYtD9GClOz/VotmUMAJIpATIdANIRGQ5AMuTr9P4AaYj0flKIIMJzDBuOh0gaS4caNdUJKPMF6T5nAsbBeGmoCVORIGmOwCLow0OASnUDU8qT0JgyTiQqxRjh/MGXofHXEdB4cIQUitdPgzFluBiXOuSGaYeJ6Q0F7yK5BZOhESjttKYytYQpf7Y0aM6mS2VOGyCCP+zYFFpADuO71IHytYN4LcEBDOdzxHwu5czPMh9ZtM2WH9V+277JZbftticqyR80lPzmNJVoB/1CtM5aUuBMoMAZBBiL50t2ZH0WWJ7ZhBanXe1gYPOGginnabiU6Q5nUp+HEymToTxxBnuuOl50mlcdjb6YHrWsAm1BMd+MpPWR4Vt/Um0P81Ft3qhShYYGLvBXq6db4OMfMj06NnYpI2urpOSUz4RnrcGSLi8n3Y9CDSysnqsqj67bEbZWFbH9B1XSnm9UhyOWqXJi5qsKYhfsy4leVFqRH1gn5H2hJgmqkqfAH4f+ChfT3cGQhZo81w34fBeAfKd2of8jRpo1ijzBmDNaajdjPfDaJ06SAqeN2LafEq3LUwA9OmwGCKv+25aopHT4o1zRc/P4YrcwxCX+yDBCioYBKXYDKXxCxlAwlLwtaZv6MmCLngJjwbNQlzMeTqROgbLkmedy4xady49duDtp79fbd29Zrd60wcczICjIE8ky2Xvr1vsFhISE9KZZKqgth39Xzg64un6qeTeJDuqAR2KlmDdLZVnKvBURGBg4KTRU5anZ+ovn/t3fqjOjl20vPTAn/Mj+d85VHZx67kzGK3Rj7jigtU8BX4htdTZRImrxQmyncc1EPTIc4HdXhIsYkkIXA3/EdRd3ZMg3TInbS2hS3asQVegA+tJIUj5mLK8b680fG6Pjy0czpHw0kGPuQMpbYgyw5c9DvXYSVGbM4QpT/s0J2ozGaTcpflsoaryPAwI2uAiwpzyCA/9aEBWn2f4Yr0KIy/FMlGDntUtUln1DXgjVCs73jtbJW721f8CGDS6bN/rO0mz/34rGhjNiGYtTl9UeO7yIsxCVw/YkZdjepaNkjJQh3fNHR14gZe4RROf+MZS7D7fWuLc8UfUXT2LnIsHQtjNVTKvjq14CvvpFhCVEVI2HhpLX4Hj2XChM+ag2Ne7L3DjNj6tCg/0/V6nV49euX99X2GuX9qFF18vHnSmL4Di3+v1WtxG1tra2N5ZVZ9lI6AhJZXIPbbYnmb90buHDuMt5mAUfrcFsEBdTNN7+GrvaOz9haXhlxnu5VVlzoKF4MnAVLwCpGCehUoZ4Pxb4irGlfOU4NVP1/Mu3JFEvlE3vw1ZPnMrW/D3MWDmNE1fKqAmNlbOAq5kMet0MqM5fAL+nLqk5GPv1r1vCfNbhFP2mn5/fg15eXq366RiW2im7WU52xp+HpInsbqJiuXrid/bImrHaaDQ62bVo4egceUcqrROr8zuFrVY5fWpLP6r1Ykqj0dylClU9vnfnurczE78MPpr+Qe5p7Zzzl8tmAntiEvAnJyImyOFEMNSqpO1cui4K6ic+dNOSEwo+6klXzBrNVs3YyVRNN3LV00GAQfe25NJhzZCd/NOJ8B2/bFQHqz/y9V3v4OHhcZc93yCEcsOO4GSb73t70nIc82nTtmQ3EVV04/DMLstgshwFtIuohH1dTi/80tXDTheeR3N9qLfaIqotQfL22qr27h+5a+VHecn/0lRkv19+ofgdmqqaBo2nVJKtezEOifyaial5Yy9V/fpUKPHodVMQlBS+5cQVzVnBl3qcIqUeaAN5gLl4LujSF0N67FdVcZHB8YL/sKsOTiNB1ZY9bY6jPTtmlzKLr9wq7XqiYj29LGWzZ5vShuY/JOejJ4Qe3aF2AXamZRAzLBNmcezbQ1RbEhAQMCBq98qXT1Xniae4Gs8mAKt7E3jdbDQNZgJ3bPYffPlsbyib4XrjaU+Nx12kYP6rfP57u/jf5hFO+x7UZX0AhQlfGhL2/BATHBz4hXdAgEtXOvxb8ekJ2IQdYJMUDGHGYOdFWe3M/IEkSOgOouKgWSh/g0JtNu2q2hegN83TK4VzsjgTLO2AbdsHB/Bqy9naFnb5k5bnV/N/CFi/D9CUKWzU14dF7V49rzBp+foTaUtO67ULgRS9C6T4XeB/n6tFXsxrLJj72PXVnhlf9jEe/vhNOuv9cib7fbh4eCnkR391Yfe2NRtUgcHvhISE9OyunakWHXkHK53ssRyDE6b0MsTOZtC5LbRogfA3PO2dR+0MUYW0zaeMxF8xLLpRgJr9SZPJNLirgG34qLWdGxbmOyx577ffVyd/llqX9qGZz1sIfN4CE5+7IIDkzBtxTQlaG+PV+3Ti6hVnkr7X18R7QU74z4bwzaqNwcHBE3ABdM+12EJt1Z7jmJI2Dv4S4UQRR+gPdTrdPbKvskuJis+ebj6yd1ug1VX/pk0+D+/fseqLktivs84c+CdzOWUpezlleda55P++1JkzBnYQNKT3qfh1y2v2r7uQt9cPojYHZPmrgxf6+2/p8KkevV7fH2237wTgtUN3lJNl2b9ZvmENJNNzNuzVz1Hbpgl26yW49LB08Bjvmfb/eE343ZYQV4CZmIfKR/KiLM+uANfKc3twHfP48+KzfaJaCyqxgQd2+H1SEL4273jsGjgdv6bkdIKPsKboOsKWRa3ucypu3XJdlM/ZX3f6X9oUqvb3DQoa1kOR20oELWgL9uazOSjIKX23atXRSJ+qmrh11bUJPnMLbJiJHZZDh7zuPrXf57WiSL/imK0Blf5q9QdeGk0vpcsU6QoRfOThW9QTtfv8EnUxPhUn49fM08W3bTpeIecP+A4rjvDNj9jinxMYHPyKl5dCUEW6T7aFhjpn7PHzL4nwLa+JW+PerkkAqEWPRfl8GLct0D8gIHiK0oSKXEsRDgrlaPw+PxLhG3Iydq3tXbzLiWv7Zu9RfagKDHy9S41cRRSxn7A9MzWqFRl7fOcLaySbEZRmUuRGkXg/v3sO7vKZnbbL301pDUVueMG1kugC/T+Qf4ayXaBvkgAAAABJRU5ErkJggg=='); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</html>
|
||||
<main class="container">
|
||||
<header>
|
||||
<picture style="display: inline-flex; justify-content: center;">
|
||||
<img class="con-logo" style="height:2rem;text-align:center;">
|
||||
</picture>
|
||||
</header>
|
||||
<article>
|
||||
<h2>{{title}}</h2>
|
||||
<p>{{body}}</p>
|
||||
</article>
|
||||
</main>
|
|
@ -17,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/noseocount/'">
|
||||
onload="window.location.href = '/manage/nosecount/'">
|
||||
</picture>
|
||||
</header>
|
||||
</main>
|
||||
|
|
26
utils.py
26
utils.py
|
@ -3,7 +3,7 @@ from sanic import exceptions
|
|||
from config import *
|
||||
import httpx
|
||||
from email.mime.text import MIMEText
|
||||
from messages import ROOM_ERROR_MESSAGES
|
||||
from messages import ROOM_ERROR_TYPES
|
||||
import smtplib
|
||||
|
||||
METADATA_TAG = "meta_data"
|
||||
|
@ -164,7 +164,8 @@ async def validate_room(request, order, om):
|
|||
check, room_errors, member_orders = await check_room(request, order, om)
|
||||
if check == True: return
|
||||
print(f'[ROOM VALIDATION FAILED] {order.code} has failed room validation.', room_errors)
|
||||
order.set_room_errors(room_errors)
|
||||
order.room_errors = room_errors
|
||||
om.add_cache (order)
|
||||
|
||||
# End here if room is not confirmed
|
||||
if not order.room_confirmed: return
|
||||
|
@ -175,25 +176,10 @@ async def validate_room(request, order, om):
|
|||
# Build message
|
||||
issues_str = ""
|
||||
for err in room_errors:
|
||||
if err in ROOM_ERROR_MESSAGES:
|
||||
issues_str += f" - {ROOM_ERROR_MESSAGES[err]}"
|
||||
if err in ROOM_ERROR_TYPES:
|
||||
issues_str += f" - {ROOM_ERROR_TYPES[err]}"
|
||||
|
||||
memberMessages = []
|
||||
|
||||
for member in member_orders:
|
||||
msg_text = f"Hello {member.name}!\n\n"
|
||||
msg_text += f"We had to unconfirm your room '{order.room_name}'"
|
||||
msg_text += f" due to th{'ese issues' if len(room_errors) > 1 else 'is issue'}:\n{issues_str}\n\n"
|
||||
msg_text += f"Please contact your room's owner or contact our support for further informations at https://furizon.net/contact/.\nThank you"
|
||||
msg = MIMEText(msg_text)
|
||||
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, 587)
|
||||
s = smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT)
|
||||
s.login(SMTP_USER, SMTP_PASSWORD)
|
||||
for message in memberMessages:
|
||||
s.sendmail(message['From'], message['to'], message.as_string())
|
||||
|
|
Loading…
Reference in New Issue