Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ jobs:
run: |
bench get-app buzz $GITHUB_WORKSPACE
bench get-app payments
bench get-app zoom_integration https://github.com/bwhtech/zoom_integration
bench setup requirements --dev
bench new-site --db-root-password root --admin-password admin test_site
bench --site test_site install-app zoom_integration
bench --site test_site install-app buzz
bench build
env:
Expand Down
21 changes: 11 additions & 10 deletions buzz/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
get_payment_link_for_booking,
get_payment_link_for_sponsorship,
)
from buzz.utils import build_event_datetimes, is_app_installed
from buzz.utils import ZOOM_BACKED_CATEGORIES, build_event_datetimes, is_app_installed

OFFLINE_PAYMENT_METHOD = "Offline"

Expand Down Expand Up @@ -239,7 +239,7 @@ def get_event_booking_data(event_route: str) -> dict:
"category": event_doc.category,
"banner_image": event_doc.banner_image,
"short_description": event_doc.short_description,
"free_webinar": event_doc.free_webinar,
"free_event": event_doc.free_event,
"send_ticket_email": event_doc.send_ticket_email,
"allow_guest_booking": event_doc.allow_guest_booking,
"guest_verification_method": event_doc.guest_verification_method,
Expand Down Expand Up @@ -438,10 +438,10 @@ def process_booking(
)
phone_map = {cf["fieldname"]: cf["label"] for cf in phone_fields}

if event_doc.category == "Webinars":
if event_doc.category in ZOOM_BACKED_CATEGORIES:
for attendee in attendees:
if not (attendee.get("last_name") or "").strip():
frappe.throw(_("Last name is required for all attendees in webinar events"))
frappe.throw(_("Last name is required for all attendees in Zoom events"))

for attendee in attendees:
first_name = (attendee.get("first_name") or "").strip()
Expand Down Expand Up @@ -588,7 +588,7 @@ def get_booking_confirmation(booking_id: str, token: str | None = None) -> dict:
"start_time": event_doc.start_time,
"end_time": event_doc.end_time,
"short_description": event_doc.get("short_description"),
"free_webinar": event_doc.get("free_webinar"),
"free_event": event_doc.get("free_event"),
},
"venue": venue,
"booking": {
Expand Down Expand Up @@ -1036,16 +1036,17 @@ def get_ticket_details(ticket_id: str) -> dict:
)

details.zoom_join_url = None
if hasattr(ticket_doc, "zoom_webinar_registration") and ticket_doc.zoom_webinar_registration:
if hasattr(ticket_doc, "zoom_session_registration") and ticket_doc.zoom_session_registration:
zoom_registration = frappe.db.get_value(
"Zoom Webinar Registration",
ticket_doc.zoom_webinar_registration,
["join_url", "webinar"],
"Zoom Session Registration",
ticket_doc.zoom_session_registration,
["join_url", "reference_doctype", "reference_name"],
as_dict=True,
)
if zoom_registration:
details.zoom_join_url = zoom_registration.join_url
details.zoom_webinar = zoom_registration.webinar
details.zoom_reference_doctype = zoom_registration.reference_doctype
details.zoom_reference_name = zoom_registration.reference_name

return details

Expand Down
35 changes: 22 additions & 13 deletions buzz/events/doctype/buzz_event/buzz_event.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// Copyright (c) 2025, BWH Studios and contributors
// For license information, please see license.txt

// Keep in sync with ZOOM_BACKED_CATEGORIES in buzz/utils.py
const ZOOM_SESSION_BY_CATEGORY = {
Webinars: "webinar",
"Zoom Meeting": "meeting",
};

const FIELD_LABELS = {
category: __("Category"),
host: __("Host"),
Expand Down Expand Up @@ -353,36 +359,39 @@ frappe.ui.form.on("Buzz Event", {

add_zoom_custom_actions(frm) {
const installed_apps = frappe.boot.app_data.map((app) => app.app_name);
if (!installed_apps.includes("zoom_integration") || frm.doc.category != "Webinars") {
const session = ZOOM_SESSION_BY_CATEGORY[frm.doc.category];
if (!installed_apps.includes("zoom_integration") || !session) {
return;
}

if (frm.doc.zoom_webinar) {
frm.add_custom_button(__("View Webinar on Zoom"), () => {
window.open(`https://zoom.us/webinar/${frm.doc.zoom_webinar}`, "_blank");
const labels = {
webinar: { create: __("Create Webinar on Zoom"), view: __("View Webinar") },
meeting: { create: __("Create Meeting on Zoom"), view: __("View Meeting") },
}[session];

const existing = frm.doc[`zoom_${session}`];
if (existing) {
frm.add_custom_button(labels.view, () => {
window.open(`https://zoom.us/${session}/${existing}`, "_blank");
});
return;
}

const btn = frm.add_custom_button(__("Create Webinar on Zoom"), () => {
const create_btn = frm.add_custom_button(labels.create, () => {
frm.call({
doc: frm.doc,
method: "create_webinar_on_zoom",
btn,
method: `create_${session}_on_zoom`,
btn: create_btn,
freeze: true,
}).then(({ message }) => {
}).then(() => {
frm.layout.tabs.find((t) => t.label == "Zoom Integration").set_active();
});
});
},
category(frm) {
if (!frm.is_new()) return;

if (frm.doc.category === "Webinars") {
frm.set_value("attach_email_ticket", 0);
} else {
frm.set_value("attach_email_ticket", 1);
}
frm.set_value("attach_email_ticket", frm.doc.category in ZOOM_SESSION_BY_CATEGORY ? 0 : 1);
},
});

Expand Down
8 changes: 4 additions & 4 deletions buzz/events/doctype/buzz_event/buzz_event.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"field_order": [
"title",
"category",
"free_webinar",
"free_event",
"medium",
"column_break_cxqh",
"banner_image",
Expand Down Expand Up @@ -390,10 +390,10 @@
},
{
"default": "0",
"depends_on": "eval:doc.category==\"Webinars\"",
"fieldname": "free_webinar",
"depends_on": "eval:[\"Webinars\",\"Zoom Meeting\"].includes(doc.category)",
"fieldname": "free_event",
"fieldtype": "Check",
"label": "Free Webinar?"
"label": "Free Event?"
},
{
"fieldname": "connections_tab",
Expand Down
46 changes: 45 additions & 1 deletion buzz/events/doctype/buzz_event/buzz_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class BuzzEvent(Document):
end_time: DF.Time
external_registration_page: DF.Check
featured_speakers: DF.Table[EventFeaturedSpeaker]
free_webinar: DF.Check
free_event: DF.Check
guest_verification_method: DF.Literal["None", "Email OTP", "Phone OTP"]
host: DF.Link
is_published: DF.Check
Expand Down Expand Up @@ -236,8 +236,30 @@ def create_webinar_on_zoom(self):

return zoom_webinar

@frappe.whitelist()
@only_if_app_installed("zoom_integration", raise_exception=True)
def create_meeting_on_zoom(self):
if not self.end_time:
frappe.throw(_("End time is needed for Zoom Meeting creation"))

zoom_meeting = frappe.get_doc(
{
"doctype": "Zoom Meeting",
"title": self.title,
"date": self.start_date,
"start_time": self.start_time,
"duration": int(time_diff_in_seconds(self.end_time, self.start_time)),
"timezone": self.time_zone,
}
).insert()

self.db_set("zoom_meeting", zoom_meeting.name)

return zoom_meeting

def on_update(self):
self.update_zoom_webinar()
self.update_zoom_meeting()

@only_if_app_installed("zoom_integration")
def update_zoom_webinar(self):
Expand All @@ -261,6 +283,28 @@ def update_zoom_webinar(self):
)
webinar.save()

@only_if_app_installed("zoom_integration")
def update_zoom_meeting(self):
if not self.zoom_meeting:
return

if (
self.has_value_changed("start_date")
or self.has_value_changed("end_time")
or self.has_value_changed("start_time")
or self.has_value_changed("time_zone")
):
meeting = frappe.get_doc("Zoom Meeting", self.zoom_meeting)
meeting.update(
{
"date": self.start_date,
"start_time": self.start_time,
"duration": int(time_diff_in_seconds(self.end_time, self.start_time)),
"timezone": self.time_zone,
}
)
meeting.save()


@frappe.whitelist()
def create_from_template(template_name: str, options: str, additional_fields: str = "{}") -> str:
Expand Down
79 changes: 79 additions & 0 deletions buzz/events/doctype/buzz_event/test_buzz_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,3 +1022,82 @@ def test_backfill_patch_skips_events_missing_start_fields(self):
backfill_time_zone_labels()

self.assertEqual(frappe.db.get_value("Buzz Event", event.name, "time_zone_label"), "")


class TestBuzzEventZoomMeeting(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
if not frappe.db.exists("Event Category", "Test Category"):
frappe.get_doc({"doctype": "Event Category", "category_name": "Test Category"}).insert(
ignore_permissions=True
)
if not frappe.db.exists("Event Host", "Test Host"):
frappe.get_doc({"doctype": "Event Host", "host_name": "Test Host"}).insert(
ignore_permissions=True
)

def tearDown(self):
frappe.db.rollback()

def _make_event(self):
return frappe.get_doc(
{
"doctype": "Buzz Event",
"title": "Meeting Event",
"category": "Test Category",
"host": "Test Host",
"start_date": "2026-08-01",
"end_date": "2026-08-01",
"start_time": "10:00:00",
"end_time": "11:00:00",
}
).insert(ignore_permissions=True)

def test_create_meeting_on_zoom_links_meeting_to_event(self):
from zoom_integration.tests.zoom_fixtures import create_meeting_response

meeting_controller = "zoom_integration.zoom_integration.doctype.zoom_meeting.zoom_meeting"
event = self._make_event()
response = create_meeting_response()

with patch(f"{meeting_controller}.create_zoom_session", return_value=response):
meeting = event.create_meeting_on_zoom()

self.assertTrue(meeting.name)
event.reload()
self.assertEqual(event.zoom_meeting, meeting.name)
self.assertEqual(meeting.zoom_meeting_id, str(response["id"]))

def test_event_stores_the_zoom_meeting_id_the_desk_link_is_built_from(self):
"""buzz_event.js builds https://zoom.us/meeting/<zoom_meeting> from this field."""
from zoom_integration.tests.zoom_fixtures import create_meeting_response

meeting_controller = "zoom_integration.zoom_integration.doctype.zoom_meeting.zoom_meeting"
event = self._make_event()
response = create_meeting_response()

with patch(f"{meeting_controller}.create_zoom_session", return_value=response):
event.create_meeting_on_zoom()

event.reload()
self.assertEqual(event.zoom_meeting, str(response["id"]))

def test_update_event_schedule_pushes_to_zoom_meeting(self):
from zoom_integration.tests.zoom_fixtures import CREATE_MEETING_RESPONSE

meeting_controller = "zoom_integration.zoom_integration.doctype.zoom_meeting.zoom_meeting"
event = self._make_event()

with patch(f"{meeting_controller}.create_zoom_session", return_value=CREATE_MEETING_RESPONSE):
event.create_meeting_on_zoom()

# Note: do not reload() — Time fields come back as timedelta and trip event
# validation's time diff. The in-memory doc keeps string times and has
# zoom_meeting set via db_set already.
with patch(f"{meeting_controller}.update_zoom_session") as mock_update:
event.end_time = "12:00:00"
event.save(ignore_permissions=True)

mock_update.assert_called_once()
self.assertEqual(mock_update.call_args.args[0], "meetings")
17 changes: 5 additions & 12 deletions buzz/events/doctype/event_category/test_event_category.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
# Copyright (c) 2025, BWH Studios and Contributors
# See license.txt

# import frappe
import frappe
from frappe.tests import IntegrationTestCase

# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
from buzz.utils import ZOOM_BACKED_CATEGORIES


class IntegrationTestEventCategory(IntegrationTestCase):
"""
Integration tests for EventCategory.
Use this class for testing interactions between multiple components.
"""

pass
def test_zoom_backed_categories_are_seeded(self):
for category in ZOOM_BACKED_CATEGORIES:
self.assertTrue(frappe.db.exists("Event Category", category), category)
23 changes: 20 additions & 3 deletions buzz/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@
"options": "Zoom Webinar",
"insert_after": "zoom_integration_tab",
},
{
"fieldname": "zoom_meeting",
"label": "Zoom Meeting",
"fieldtype": "Link",
"options": "Zoom Meeting",
"insert_after": "zoom_webinar",
},
],
"Buzz Settings": [
{
Expand All @@ -67,10 +74,10 @@
],
"Event Ticket": [
{
"fieldname": "zoom_webinar_registration",
"label": "Zoom Webinar Registration",
"fieldname": "zoom_session_registration",
"label": "Zoom Session Registration",
"fieldtype": "Link",
"options": "Zoom Webinar Registration",
"options": "Zoom Session Registration",
"insert_after": "ticket_type",
"read_only": 1,
},
Expand Down Expand Up @@ -121,6 +128,8 @@ def after_install():


def on_migrate():
# insert is ignore_if_duplicate, so this only fills in categories added since install
create_event_categories()
create_talk_proposal_statuses()
create_custom_fields()

Expand Down Expand Up @@ -209,6 +218,14 @@ def create_event_categories():
"name": "Webinars",
"icon_svg": """<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36.9709 2H3.03063C1.35851 2 0 3.35681 0 5.03063V26.8484C0 28.5206 1.35681 29.8791 3.03063 29.8791H19.394V35.9405H15.1514C14.8122 35.9405 14.5446 36.2064 14.5446 36.5473C14.5446 36.8865 14.8105 37.1541 15.1514 37.1541H24.8486C25.1878 37.1541 25.4554 36.8882 25.4554 36.5473C25.4554 36.2081 25.1895 35.9405 24.8486 35.9405H20.606V29.8791H36.9694C38.6415 29.8791 40 28.5223 40 26.8484V5.03063C40 3.35851 38.6432 2 36.9694 2H36.9709ZM38.7879 26.8483C38.7879 27.854 37.9765 28.667 36.9692 28.667L3.03067 28.6653C2.025 28.6653 1.21194 27.854 1.21194 26.8466L1.21364 5.03054C1.21364 4.02487 2.025 3.2118 3.03237 3.2118H36.9709C37.9765 3.2118 38.7896 4.02316 38.7896 5.03054V26.8484L38.7879 26.8483ZM11.5156 16.5446C7.84237 16.5446 4.8494 19.5378 4.8494 23.2108V24.4227C4.8494 24.7619 5.11531 25.0295 5.4562 25.0295H17.5769C17.9161 25.0295 18.1837 24.7636 18.1837 24.4227V23.2108C18.1837 19.5376 15.1905 16.5446 11.5175 16.5446H11.5156ZM16.9701 23.8178H6.06116V23.211C6.06116 20.2043 8.50886 17.7566 11.5156 17.7566C14.5224 17.7566 16.9701 20.2043 16.9701 23.211V23.8178ZM11.5156 15.3329C13.8542 15.3329 15.7582 13.4306 15.7582 11.0903C15.7582 8.7517 13.8559 6.84774 11.5156 6.84774C9.17701 6.84774 7.27306 8.75 7.27306 11.0903C7.27306 13.4289 9.17531 15.3329 11.5156 15.3329ZM11.5156 8.05968C13.1877 8.05968 14.5462 9.41649 14.5462 11.0903C14.5462 12.7624 13.1894 14.1209 11.5156 14.1209C9.8435 14.1209 8.48499 12.7641 8.48499 11.0903C8.48499 9.41819 9.8418 8.05968 11.5156 8.05968ZM34.8489 14.3628L26.2924 9.41806C25.7231 9.09079 25.0447 9.09079 24.4737 9.41806C23.9043 9.74533 23.5652 10.3385 23.5652 10.993V20.8844C23.5652 21.5389 23.9044 22.1321 24.4737 22.4594C24.7651 22.6298 25.0668 22.7014 25.3822 22.7014C25.6975 22.7014 26.0128 22.6162 26.2906 22.4594L34.8472 17.5146C35.4165 17.1873 35.7556 16.5941 35.7556 15.9396C35.7556 15.285 35.4165 14.6919 34.8472 14.3646L34.8489 14.3628ZM34.2438 16.4594L25.6873 21.4042C25.4078 21.561 25.1776 21.4519 25.0805 21.4042C24.9834 21.3565 24.7771 21.198 24.7771 20.8826V10.9913C24.7771 10.676 24.9833 10.5192 25.0805 10.4697C25.1419 10.4339 25.2509 10.3845 25.3839 10.3845C25.4691 10.3845 25.5782 10.4084 25.6873 10.4697L34.2438 15.4145C34.5233 15.5713 34.5472 15.8389 34.5472 15.9361C34.5472 16.0332 34.5234 16.2991 34.2438 16.4577L34.2438 16.4594Z" fill="#999999"/>
</svg>""",
"enabled": 1,
},
{
"name": "Zoom Meeting",
"icon_svg": """<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 9.5C2.11929 9.5 1 10.6193 1 12V28C1 29.3807 2.11929 30.5 3.5 30.5H24.5C25.8807 30.5 27 29.3807 27 28V12C27 10.6193 25.8807 9.5 24.5 9.5H3.5Z" stroke="#191919" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M27 16.5L36.2 11.3C37.2 10.8 38.5 11.5 38.5 12.6V27.4C38.5 28.5 37.2 29.2 36.2 28.7L27 23.5V16.5Z" stroke="#191919" stroke-width="1.5" stroke-linejoin="round"/>
</svg>""",
"enabled": 1,
},
Expand Down
2 changes: 2 additions & 0 deletions buzz/patches.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ buzz.patches.set_applies_to_for_existing_coupons
buzz.patches.set_payment_status_for_existing_bookings
buzz.patches.normalize_phone_format
buzz.patches.set_time_zone_label_for_existing_events
buzz.patches.rename_zoom_registration_field
buzz.patches.rename_free_webinar_to_free_event
Loading
Loading