diff --git a/buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py b/buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py index eb5975e7..89791d04 100644 --- a/buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py +++ b/buzz/proposals/doctype/sponsorship_enquiry/sponsorship_enquiry.py @@ -2,11 +2,11 @@ # For license information, please see license.txt import frappe -from frappe.email.doctype.email_template.email_template import get_email_template from frappe.model.document import Document from frappe.utils import get_url from buzz.payments import mark_payment_as_received +from buzz.utils import render_email_template class SponsorshipEnquiry(Document): @@ -91,7 +91,7 @@ def send_pitch_deck(self, now=False): frappe.log_error("No sponsor deck email template configured", "Sponsorship Enquiry") return - email_template = get_email_template(template_name, {"doc": self, "event": event}) + email_template = render_email_template(template_name, {"doc": self, "event": event}) subject = email_template.get("subject") content = email_template.get("message") diff --git a/buzz/ticketing/doctype/event_booking/event_booking.py b/buzz/ticketing/doctype/event_booking/event_booking.py index 10cae89a..50f5c3d2 100644 --- a/buzz/ticketing/doctype/event_booking/event_booking.py +++ b/buzz/ticketing/doctype/event_booking/event_booking.py @@ -2,7 +2,6 @@ # For license information, please see license.txt import frappe from frappe import _ -from frappe.email.doctype.email_template.email_template import get_email_template from frappe.model.document import Document from frappe.utils import cstr, flt @@ -13,6 +12,7 @@ get_committed_tickets, record_gateway_refund, ) +from buzz.utils import render_email_template RAZORPAY = "Razorpay" @@ -222,7 +222,7 @@ def send_booking_confirmation_email(self): content = None if booking_template: - email_template = get_email_template(booking_template, args) + email_template = render_email_template(booking_template, args) subject = email_template.get("subject") or subject content = email_template.get("message") diff --git a/buzz/ticketing/doctype/event_ticket/event_ticket.py b/buzz/ticketing/doctype/event_ticket/event_ticket.py index e198d311..fc4bc4db 100644 --- a/buzz/ticketing/doctype/event_ticket/event_ticket.py +++ b/buzz/ticketing/doctype/event_ticket/event_ticket.py @@ -5,7 +5,12 @@ from frappe.core.api.user_invitation import invite_by_email from frappe.model.document import Document -from buzz.utils import generate_ics_file, generate_qr_code_file, only_if_app_installed +from buzz.utils import ( + generate_ics_file, + generate_qr_code_file, + only_if_app_installed, + render_email_template, +) class EventTicket(Document): @@ -122,9 +127,7 @@ def send_ticket_email(self, now: bool = False): } if ticket_template: - from frappe.email.doctype.email_template.email_template import get_email_template - - email_template = get_email_template(ticket_template, args) + email_template = render_email_template(ticket_template, args) subject = email_template.get("subject") content = email_template.get("message") diff --git a/buzz/ticketing/doctype/event_ticket/test_event_ticket.py b/buzz/ticketing/doctype/event_ticket/test_event_ticket.py index 3d7a47e4..cdf4a27b 100644 --- a/buzz/ticketing/doctype/event_ticket/test_event_ticket.py +++ b/buzz/ticketing/doctype/event_ticket/test_event_ticket.py @@ -6,7 +6,7 @@ import frappe from frappe.tests import IntegrationTestCase -from buzz.utils import generate_qr_code_file, make_qr_image +from buzz.utils import generate_qr_code_file, make_qr_image, render_email_template EXTRA_TEST_RECORD_DEPENDENCIES = [] IGNORE_TEST_RECORD_DEPENDENCIES = ["Bulk Ticket Coupon"] @@ -304,3 +304,87 @@ def test_ticket_details_expose_the_zoom_session_reference(self): self.assertEqual(details.zoom_join_url, registrant["join_url"]) self.assertEqual(details.zoom_reference_doctype, "Zoom Meeting") self.assertEqual(details.zoom_reference_name, meeting.name) + + +class TestRenderEmailTemplate(IntegrationTestCase): + """Attendees and sponsors are Website Users; only Desk Users can read an Email Template.""" + + def tearDown(self): + frappe.db.rollback() + + def test_renders_for_a_user_without_email_template_permission(self): + template = frappe.get_doc( + { + "doctype": "Email Template", + "name": "Unprivileged Render Template", + "subject": "NOPERM - {{ event_title }}", + "response": "

NOPERM content

", + } + ).insert(ignore_permissions=True) + + attendee = "attendee-render@example.com" + if not frappe.db.exists("User", attendee): + frappe.get_doc( + { + "doctype": "User", + "email": attendee, + "first_name": "Attendee", + "user_type": "Website User", + "send_welcome_email": 0, + } + ).insert(ignore_permissions=True) + self.addCleanup(frappe.set_user, frappe.session.user) + frappe.set_user(attendee) + self.assertFalse(frappe.has_permission("Email Template", "read")) + + rendered = render_email_template(template.name, {"event_title": "Buzz Conf"}) + + self.assertEqual(rendered["subject"], "NOPERM - Buzz Conf") + self.assertIn("NOPERM content", rendered["message"]) + + +class TestGuestTicketEmail(IntegrationTestCase): + """Public bookings submit their tickets as Guest.""" + + def setUp(self): + self.event = frappe.get_doc("Buzz Event", {"route": "test-route"}) + self.ticket_type = frappe.get_doc( + { + "doctype": "Event Ticket Type", + "event": self.event.name, + "title": "Guest Email Ticket", + "price": 0, + } + ).insert(ignore_permissions=True) + self.template = frappe.get_doc( + { + "doctype": "Email Template", + "name": "Guest Ticket Template", + "subject": "GUEST - {{ event_title }}", + "response": "

Guest content

", + } + ).insert(ignore_permissions=True) + self.event.db_set("ticket_email_template", self.template.name) + self.addCleanup(frappe.set_user, frappe.session.user) + + def tearDown(self): + frappe.db.rollback() + + @patch("frappe.sendmail") + def test_guest_can_render_the_ticket_email_template(self, mock_sendmail): + ticket = frappe.get_doc( + { + "doctype": "Event Ticket", + "event": self.event.name, + "ticket_type": self.ticket_type.name, + "first_name": "Guest", + "attendee_email": "guest-booking@example.com", + } + ).insert(ignore_permissions=True) + + frappe.set_user("Guest") + + ticket.send_ticket_email(now=True) + + mock_sendmail.assert_called_once() + self.assertIn("GUEST", mock_sendmail.call_args[1]["subject"]) diff --git a/buzz/utils.py b/buzz/utils.py index 3b982e03..3a5780cf 100644 --- a/buzz/utils.py +++ b/buzz/utils.py @@ -278,3 +278,12 @@ def get_time_zone_label(time_zone: str | None, reference_datetime: datetime | No if minutes: label += f":{minutes:02d}" return label + + +def render_email_template(template_name: str, args: dict) -> dict: + """Render an Email Template without a permission check. + + Frappe's get_email_template() is whitelisted and reads the template as the session + user, which fails for the guest-facing booking and enquiry flows. + """ + return frappe.get_doc("Email Template", template_name).get_formatted_email(args)