[wip6] EMail sending re-ordering

This commit is contained in:
Andrea Carulli 2024-01-19 18:03:54 +01:00
parent 16ae51fb08
commit a154c3059b
11 changed files with 193 additions and 117 deletions

4
app.py
View File

@ -139,8 +139,10 @@ async def welcome(request, order: Order, quota: Quotas):
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))
print (order.room_errors)
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, 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") @app.route("/manage/download_ticket")

View File

@ -117,5 +117,6 @@ TG_CHAT_ID = -1234567
ADMINS = ['XXXXX', 'YYYYY'] ADMINS = ['XXXXX', 'YYYYY']
SMTP_HOST = 'host' SMTP_HOST = 'host'
SMTP_PORT = 0
SMTP_USER = 'user' SMTP_USER = 'user'
SMTP_PASSWORD = 'pw' SMTP_PASSWORD = 'pw'

37
email_util.py Normal file
View File

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

@ -98,7 +98,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_issues = [] 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')
@ -133,7 +133,7 @@ class Order:
return self.data[var] return self.data[var]
def set_room_errors (self, to_set): 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): def ans(self, name):
for p in self.data['positions']: for p in self.data['positions']:

90
image_util.py Normal file
View File

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

View File

@ -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.", '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.", '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.", '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
View File

@ -3,11 +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
from PIL import Image, ImageDraw, ImageFont
import textwrap
import os import os
from image_util import generate_room_preview, get_room
jobs = []
bp = Blueprint("room", url_prefix="/manage/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') 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): async def get_room_with_order (request, code):
order_data = await request.app.ctx.om.get_order(code=code) order_data = await request.app.ctx.om.get_order(code=code)
if not order_data or not order_data.room_owner: return None 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: except Exception as ex:
if (EXTRA_PRINTS): print(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>") @bp.route("/view/<code>")
async def get_view(request, code): async def get_view(request, code):
room_file_name = f"res/rooms/{code}.jpg" 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) await generate_room_preview(request, code, room_data)
tpl = request.app.ctx.tpl.get_template('view_room.html') tpl = request.app.ctx.tpl.get_template('view_room.html')
return html(tpl.render(preview=room_file_name, room_data=room_data)) return html(tpl.render(preview=room_file_name, room_data=room_data))

View File

@ -14,7 +14,8 @@
{% if order.room_id and not order.room_confirmed %} {% 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> <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 %} {% 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: <p class="notice">⚠️ There are some issues with your room:
<ul> <ul>
{% for issue in issues %} {% for issue in issues %}

View File

@ -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,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'); }
}
</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>

View File

@ -17,7 +17,7 @@
<picture> <picture>
<source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)"> <source srcset="/res/furizon.png" media="(prefers-color-scheme:dark)">
<img src="/res/furizon-light.png" style="height:4rem;text-align:center;" <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> </picture>
</header> </header>
</main> </main>

View File

@ -3,7 +3,7 @@ from sanic import exceptions
from config import * from config import *
import httpx import httpx
from email.mime.text import MIMEText from email.mime.text import MIMEText
from messages import ROOM_ERROR_MESSAGES from messages import ROOM_ERROR_TYPES
import smtplib import smtplib
METADATA_TAG = "meta_data" 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) check, room_errors, member_orders = await check_room(request, order, om)
if check == True: return if check == True: return
print(f'[ROOM VALIDATION FAILED] {order.code} has failed room validation.', room_errors) 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 # End here if room is not confirmed
if not order.room_confirmed: return if not order.room_confirmed: return
@ -175,25 +176,10 @@ async def validate_room(request, order, om):
# Build message # Build message
issues_str = "" issues_str = ""
for err in room_errors: for err in room_errors:
if err in ROOM_ERROR_MESSAGES: if err in ROOM_ERROR_TYPES:
issues_str += f" - {ROOM_ERROR_MESSAGES[err]}" issues_str += f" - {ROOM_ERROR_TYPES[err]}"
memberMessages = [] s = smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT)
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.login(SMTP_USER, SMTP_PASSWORD) s.login(SMTP_USER, SMTP_PASSWORD)
for message in memberMessages: for message in memberMessages:
s.sendmail(message['From'], message['to'], message.as_string()) s.sendmail(message['From'], message['to'], message.as_string())