diff --git a/mailer/bilf-mail-council.html b/mailer/bilf-mail-council.html
index 6f39aa43..3ef05fce 100644
--- a/mailer/bilf-mail-council.html
+++ b/mailer/bilf-mail-council.html
@@ -24,7 +24,7 @@
Hej Bilf!,
En ny kollegie-bilbokning har gjorts!
- 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>
+ 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.
Med Vänliga hälsningar,
De små tomtarna som sitter i sektionens SMTP-server och matar brevduvorna
|
@@ -49,7 +49,7 @@
Hello BilF!,
A new council booking has been made!
- 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.
+ 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.
Best regards,
The little gnomes that inhabit the guilds SMTP server and feed the carrier pidgeons
|
diff --git a/mailer/bilf-mail-private.html b/mailer/bilf-mail-private.html
index 8bf565ae..ccd8a3f5 100644
--- a/mailer/bilf-mail-private.html
+++ b/mailer/bilf-mail-private.html
@@ -49,7 +49,7 @@
Hello BilF!,
A new PRIVATE booking has been made!
- The car is booked by {{ booking.name }} on {{ booking.date }} at {{ booking.time}}. TAKE CARE OF IT!
+ The car is booked by {{ booking.name }} on {{ booking.date }} at {{ booking.time }}. TAKE CARE OF IT!
Best regards,
The little gnomes that inhabit the guilds SMTP server and feed the carrier pidgeons
|
diff --git a/mailer/bilf_mailer.py b/mailer/bilf_mailer.py
index e70fb698..65049e68 100644
--- a/mailer/bilf_mailer.py
+++ b/mailer/bilf_mailer.py
@@ -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 (
@@ -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")
@@ -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)
diff --git a/services/car_renting_service.py b/services/car_renting_service.py
index f3215a2b..0da08ec9 100644
--- a/services/car_renting_service.py
+++ b/services/car_renting_service.py
@@ -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
@@ -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
diff --git a/tests/test_car_bookings.py b/tests/test_car_bookings.py
index 8d3643e9..938e3a3a 100644
--- a/tests/test_car_bookings.py
+++ b/tests/test_car_bookings.py
@@ -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)
@@ -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)