Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mailer/bilf-mail-council.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<td align="center">
<p>Hej Bilf!,</p>
<p>En ny kollegie-bilbokning har gjorts!</p>
<p>F-älgen är bokad av {{ booking.name }} från {{ booking.council_sv }} den {{ booking.date }} klockan {{ booking.time }}. Detta bör hantera sig självt men vi tänkte att du kanske vill veta./p>
<p>F-älgen är bokad av {{ booking.name }} från {{ booking.council_sv }} den {{ booking.date }} klockan {{ booking.time }}. Detta bör hantera sig självt men vi tänkte att du kanske vill veta.</p>
<p>Med Vänliga hälsningar,<br>
De små tomtarna som sitter i sektionens SMTP-server och matar brevduvorna</p>
</td>
Expand All @@ -49,7 +49,7 @@
<td align="center">
<p>Hello BilF!,</p>
<p>A new council booking has been made!</p>
<p>The car is booked by {{ booking.name }} from {{ booking.council_en }} on {{ booking.date }} at {{ booking.time}}. It should handle itself but we though you migh want to know.</p>
<p>The car is booked by {{ booking.name }} from {{ booking.council_en }} on {{ booking.date }} at {{ booking.time }}. It should handle itself, but we thought you might want to know.</p>
<p>Best regards,<br>
The little gnomes that inhabit the guilds SMTP server and feed the carrier pidgeons</p>
</td>
Expand Down
2 changes: 1 addition & 1 deletion mailer/bilf-mail-private.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<td align="center">
<p>Hello BilF!,</p>
<p>A new <strong>PRIVATE</strong> booking has been made!</p>
<p>The car is booked by {{ booking.name }} on {{ booking.date }} at {{ booking.time}}. TAKE CARE OF IT!</p>
<p>The car is booked by {{ booking.name }} on {{ booking.date }} at {{ booking.time }}. TAKE CARE OF IT!</p>
<p>Best regards,<br>
The little gnomes that inhabit the guilds SMTP server and feed the carrier pidgeons</p>
</td>
Expand Down
98 changes: 82 additions & 16 deletions mailer/bilf_mailer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import os
import html as python_html
import uuid
from datetime import datetime, timezone
from zoneinfo import ZoneInfo


from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from db_models.car_booking_model import CarBooking_DB
from mailer.mail_constants import (
Expand All @@ -11,16 +13,12 @@
from mailer.mail_core import send_mail_to_address


def bilf_mailer(booking: CarBooking_DB) -> None:

path = os.getcwd()
def render_bilf_mail(booking: CarBooking_DB) -> str:
template_name = "bilf-mail-private.html" if booking.personal else "bilf-mail-council.html"
template_path = os.path.join(os.path.dirname(__file__), template_name)

if booking.personal:
with open(f"{path}/mailer/bilf-mail-private.html", "r", encoding="utf-8") as f:
html = f.read()
else:
with open(f"{path}/mailer/bilf-mail-council.html", "r", encoding="utf-8") as f:
html = f.read()
with open(template_path, "r", encoding="utf-8") as f:
html = f.read()

stockholm_tz = ZoneInfo("Europe/Stockholm")
date_string = booking.start_time.astimezone(stockholm_tz).strftime("%Y-%m-%d")
Expand All @@ -29,22 +27,90 @@ def bilf_mailer(booking: CarBooking_DB) -> None:
html = html.replace(
"{{ booking.name }}", python_html.escape(booking.user.first_name + " " + booking.user.last_name, quote=True)
)

html = html.replace("{{ booking.date }}", date_string)

html = html.replace("{{ booking.time }}", time_string)

if booking.council is not None:
html = html.replace("{{ booking.council_en }}", python_html.escape(booking.council.name_en, quote=True))
html = html.replace("{{ booking.council_sv }}", python_html.escape(booking.council.name_sv, quote=True))
else:
html = html.replace("{{ booking.council_en }}", "Unknown council")
html = html.replace("{{ booking.council_sv }}", "Okänt utskott")

msg = MIMEText(html, "html", "utf-8")
return html


def render_bilf_ics(booking: CarBooking_DB, is_update: bool = False) -> str:
booking_id = getattr(booking, "booking_id", None)
uid = (
f"car-booking-{booking_id}@fsektionen.se"
if booking_id is not None
else f"car-booking-{uuid.uuid4()}@fsektionen.se"
)
dtstamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
dtstart = booking.start_time.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
dtend = booking.end_time.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
summary = "Updated private car booking" if is_update else "Private car booking"
name = f"{booking.user.first_name} {booking.user.last_name}"
description = (
f"Private car booking for {name}.\n"
f"Start: {booking.start_time.astimezone(ZoneInfo('Europe/Stockholm')).strftime('%Y-%m-%d %H:%M')}\n"
f"End: {booking.end_time.astimezone(ZoneInfo('Europe/Stockholm')).strftime('%Y-%m-%d %H:%M')}"
)

def escape_ics(value: str) -> str:
return value.replace("\\", "\\\\").replace(";", "\\;").replace(",", "\\,").replace("\n", "\\n")

return (
"\n".join(
[
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//F-sektionen//Car Booking Mailer//EN",
"CALSCALE:GREGORIAN",
"METHOD:REQUEST",
"BEGIN:VEVENT",
f"UID:{uid}",
f"DTSTAMP:{dtstamp}",
f"DTSTART:{dtstart}",
f"DTEND:{dtend}",
f"SUMMARY:{escape_ics(summary)}",
f"DESCRIPTION:{escape_ics(description)}",
"ORGANIZER:mailto:bil@fsektionen.se",
f"ATTENDEE;CN={escape_ics(name)}:mailto:bil@fsektionen.se",
f"SEQUENCE:{1 if is_update else 0}",
"STATUS:CONFIRMED",
"END:VEVENT",
"END:VCALENDAR",
]
)
+ "\n"
)


def bilf_mailer(booking: CarBooking_DB, is_update: bool = False) -> None:
html = render_bilf_mail(booking)

msg["From"] = STANDARD_SENDER
msg["To"] = "bil@fsektionen.se"
if booking.personal:
msg["Subject"] = "Ny PRIVAT bilbokning / New PRIVATE car booking"
msg = MIMEMultipart("mixed")
html_part = MIMEText(html, "html", "utf-8")
msg.attach(html_part)

calendar_part = MIMEText(render_bilf_ics(booking, is_update=is_update), "calendar", "utf-8")
calendar_part.set_param("method", "REQUEST")
calendar_part.add_header("Content-Disposition", "attachment; filename=bilbokning.ics")
msg.attach(calendar_part)

msg["Subject"] = (
"Updated private car booking / Uppdaterad privat bilbokning"
if is_update
else ("Ny PRIVAT bilbokning / New PRIVATE car booking")
)
else:
msg = MIMEText(html, "html", "utf-8")
msg["Subject"] = "Ny kollegie-bilbokning / New council car booking"

msg["From"] = STANDARD_SENDER
msg["To"] = "bil@fsektionen.se"

send_mail_to_address("bil@fsektionen.se", msg)
6 changes: 4 additions & 2 deletions services/car_renting_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,10 @@ def create_new_booking(
db.add(db_booking)

db.flush()
db.commit()

bilf_mailer.bilf_mailer(db_booking)

db.commit()

return db_booking


Expand Down Expand Up @@ -264,4 +263,7 @@ def booking_update(

db.commit()

if car_booking.personal:
bilf_mailer.bilf_mailer(car_booking, is_update=True)

return car_booking
51 changes: 51 additions & 0 deletions tests/test_car_bookings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# type: ignore
from types import SimpleNamespace

import pytest
from main import app
from datetime import datetime, timedelta, timezone
from mailer import bilf_mailer as bilf_mailer_module
from .basic_factories import auth_headers

# Helper to get Stockholm local time (UTC+1 or UTC+2 DST, but for simplicity, use UTC+1)
Expand Down Expand Up @@ -30,6 +33,54 @@ def patch_booking(client, token, booking_id, **kwargs):
return client.patch(f"/car/{booking_id}", json=kwargs, headers=auth_headers(token))


def test_private_booking_email_includes_ics_and_update_notice(monkeypatch):
captured = {}

def fake_send_mail_to_address(address, msg):
captured["address"] = address
captured["msg"] = msg

monkeypatch.setattr(bilf_mailer_module, "send_mail_to_address", fake_send_mail_to_address)

booking = SimpleNamespace(
booking_id=42,
personal=True,
start_time=datetime(2030, 1, 8, 9, 0, tzinfo=timezone.utc),
end_time=datetime(2030, 1, 8, 12, 0, tzinfo=timezone.utc),
user=SimpleNamespace(first_name="Ada", last_name="Lovelace"),
council=None,
)

bilf_mailer_module.bilf_mailer(booking)
assert captured["address"] == "bil@fsektionen.se"
assert captured["msg"].get_content_type() == "multipart/mixed"
calendar_part = next(part for part in captured["msg"].walk() if part.get_content_type() == "text/calendar")
payload = calendar_part.get_payload(decode=True).decode("utf-8")
assert "BEGIN:VCALENDAR" in payload
assert "UID:car-booking-42@fsektionen.se" in payload
assert "DTSTART:" in payload
assert "DTEND:" in payload

bilf_mailer_module.bilf_mailer(booking, is_update=True)
assert "Updated private car booking" in captured["msg"]["Subject"]


def test_council_booking_creation_sends_email(client, admin_token, admin_council_id, monkeypatch):
sent_bookings = []
monkeypatch.setattr(bilf_mailer_module, "bilf_mailer", lambda booking: sent_bookings.append(booking))

start = stockholm_dt(2030, 1, 8, 10)
end = stockholm_dt(2030, 1, 8, 12)
response = create_booking(
client, admin_token, start, end, "council booking", council_id=admin_council_id
)

assert response.status_code in (200, 201)
assert len(sent_bookings) == 1
assert sent_bookings[0].personal is False
assert sent_bookings[0].council_id == admin_council_id


def test_admin_autoconfirm_council(client, admin_token, admin_council_id):
start = stockholm_dt(2030, 1, 8, 10) # Tuesday
end = stockholm_dt(2030, 1, 8, 12)
Expand Down
Loading