From a154c3059be943ba998b1d526af3c5791a63da7f Mon Sep 17 00:00:00 2001 From: Andrea Carulli Date: Fri, 19 Jan 2024 18:03:54 +0100 Subject: [PATCH] [wip6] EMail sending re-ordering --- app.py | 4 +- config.example.py | 1 + email_util.py | 37 +++++++++++++++ ext.py | 4 +- image_util.py | 90 ++++++++++++++++++++++++++++++++++++ messages.py | 11 ++++- room.py | 92 +------------------------------------ tpl/blocks/room.html | 3 +- tpl/email/comunication.html | 40 ++++++++++++++++ tpl/view_room.html | 2 +- utils.py | 26 +++-------- 11 files changed, 193 insertions(+), 117 deletions(-) create mode 100644 email_util.py create mode 100644 image_util.py create mode 100644 tpl/email/comunication.html diff --git a/app.py b/app.py index d53b49b..f100edb 100644 --- a/app.py +++ b/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") diff --git a/config.example.py b/config.example.py index c39f559..6034cfa 100644 --- a/config.example.py +++ b/config.example.py @@ -117,5 +117,6 @@ TG_CHAT_ID = -1234567 ADMINS = ['XXXXX', 'YYYYY'] SMTP_HOST = 'host' +SMTP_PORT = 0 SMTP_USER = 'user' SMTP_PASSWORD = 'pw' diff --git a/email_util.py b/email_util.py new file mode 100644 index 0000000..461d3ea --- /dev/null +++ b/email_util.py @@ -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 = "" + + 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 ' + 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)) \ No newline at end of file diff --git a/ext.py b/ext.py index e9afd1d..6c09b9c 100644 --- a/ext.py +++ b/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']: diff --git a/image_util.py b/image_util.py new file mode 100644 index 0000000..09d7f58 --- /dev/null +++ b/image_util.py @@ -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} \ No newline at end of file diff --git a/messages.py b/messages.py index a9a190a..7051486 100644 --- a/messages.py +++ b/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 {0}
We had to unconfirm your room '{1}' due to the following problem/s:
{2}
Please contact your room's owner or contact our support for further informations at https://furizon.net/contact/.
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" } \ No newline at end of file diff --git a/room.py b/room.py index 8141c3a..226024e 100644 --- a/room.py +++ b/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/") async def get_view(request, code): room_file_name = f"res/rooms/{code}.jpg" @@ -460,5 +373,4 @@ async def get_view(request, code): if not os.path.exists(room_file_name) and code not in jobs: 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)) - \ No newline at end of file + return html(tpl.render(preview=room_file_name, room_data=room_data)) \ No newline at end of file diff --git a/tpl/blocks/room.html b/tpl/blocks/room.html index 82a0cb0..8d0b839 100644 --- a/tpl/blocks/room.html +++ b/tpl/blocks/room.html @@ -14,7 +14,8 @@ {% if order.room_id and not order.room_confirmed %}

⚠️ Your room hasn't been confirmed yet. Unconfirmed rooms are subject to changes by the staff as we optimize for hotel capacity.

{% 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 %}

⚠️ There are some issues with your room:

    {% for issue in issues %} diff --git a/tpl/email/comunication.html b/tpl/email/comunication.html new file mode 100644 index 0000000..06598af --- /dev/null +++ b/tpl/email/comunication.html @@ -0,0 +1,40 @@ + + + + + + + Simple Transactional Email + + + +
    +
    + + + +
    +
    +

    {{title}}

    +

    {{body}}

    +
    +
    \ No newline at end of file diff --git a/tpl/view_room.html b/tpl/view_room.html index 4ecadd9..b252840 100644 --- a/tpl/view_room.html +++ b/tpl/view_room.html @@ -17,7 +17,7 @@ + onload="window.location.href = '/manage/nosecount/'"> diff --git a/utils.py b/utils.py index 9b088d3..fe8994b 100644 --- a/utils.py +++ b/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]}" - - memberMessages = [] + if err in ROOM_ERROR_TYPES: + issues_str += f" - {ROOM_ERROR_TYPES[err]}" - 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 ' - 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())