diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.js b/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.js index 4418acb..f5e5a9c 100644 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.js +++ b/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.js @@ -9,6 +9,8 @@ frappe.ui.form.on("Zoom Webinar", { frm.call({ method: "sync_attendance_in_background", doc: frm.doc, + freeze: true, + freeze_message: __("Starting attendance sync..."), }).then(() => { frappe.show_alert({ message: __("Attendance sync has been started in the background..."), @@ -18,8 +20,10 @@ frappe.ui.form.on("Zoom Webinar", { }); frm.add_custom_button(__("Sync Registrations"), () => { frm.call({ - method: "sync_registration_in_background", + method: "sync_registrations_in_background", doc: frm.doc, + freeze: true, + freeze_message: __("Starting registrations sync..."), }).then(() => { frappe.show_alert({ message: __("Registration sync has been started in the background..."), diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.json b/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.json index cb25ac3..ac25ada 100644 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.json +++ b/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.json @@ -144,13 +144,9 @@ { "link_doctype": "Zoom Webinar Attendance Record", "link_fieldname": "webinar" - }, - { - "link_doctype": "Zoom Webinar Registrant Record", - "link_fieldname": "webinar" } ], - "modified": "2025-11-16 18:31:16.335445", + "modified": "2025-12-09 13:30:38.107775", "modified_by": "Administrator", "module": "Zoom Integration", "name": "Zoom Webinar", diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.py b/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.py index 292a893..dc7d9d8 100644 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.py +++ b/zoom_integration/zoom_integration/doctype/zoom_webinar/zoom_webinar.py @@ -191,7 +191,7 @@ def sync_attendance(self): for attendance in batch: registration = frappe.db.get_value( - "Zoom Webinar Registration", {"user": attendance.get("user_email", "N/A")}, "name" + "Zoom Webinar Registration", {"email": attendance.get("user_email", "N/A")}, "name" ) try: @@ -253,7 +253,7 @@ def sync_attendance(self): frappe.throw(_(f"Failed to sync attendance: {e!s}")) @frappe.whitelist() - def get_registrants(self): + def sync_registrations_from_zoom(self): try: details = get_webinar_registrant_details(self.name) @@ -278,27 +278,37 @@ def get_registrants(self): try: doc = frappe.get_doc( { - "doctype": "Zoom Webinar Registrant Record", + "doctype": "Zoom Webinar Registration", "registrant_id": registrant.get("id"), + "join_url": registrant.get("join_url"), "webinar": self.name, "email": registrant.get("email"), "first_name": registrant.get("first_name"), "last_name": registrant.get("last_name"), - "phone": registrant.get("phone"), "docstatus": 1, + "synced_from_zoom": 1, } ) for question in registrant.get("custom_questions"): if question.get("title"): doc.append( - "custom_question", + "additional_params", { "key": question.get("title", "N/A"), "value": question.get("value", "N/A"), }, ) - doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + if registrant.get("phone"): + doc.append( + "additional_params", + { + "key": "Phone", + "value": registrant.get("phone"), + }, + ) + + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) processed_count += 1 except Exception as e: frappe.log_error( @@ -351,19 +361,13 @@ def sync_attendance_in_background(self): "sync_attendance", queue="long", ) - frappe.enqueue_doc( - self.doctype, - self.name, - "get_registrants", - queue="long", - ) @frappe.whitelist() - def sync_registration_in_background(self): + def sync_registrations_in_background(self): frappe.enqueue_doc( self.doctype, self.name, - "get_registrants", + "sync_registrations_from_zoom", queue="long", ) diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/test_zoom_webinar_registrant_record.py b/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/test_zoom_webinar_registrant_record.py deleted file mode 100644 index 07c98a8..0000000 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/test_zoom_webinar_registrant_record.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2025, Build With Hussain and Contributors -# See license.txt - -# 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"] - - -class IntegrationTestZoomWebinarRegistrantRecord(IntegrationTestCase): - """ - Integration tests for ZoomWebinarRegistrantRecord. - Use this class for testing interactions between multiple components. - """ - - pass diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.js b/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.js deleted file mode 100644 index a067d12..0000000 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2025, Build With Hussain and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("Zoom Webinar Registrant Record", { -// refresh(frm) { - -// }, -// }); diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.json b/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.json deleted file mode 100644 index aec8a7e..0000000 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2025-11-16 17:53:45.920308", - "doctype": "DocType", - "engine": "InnoDB", - "field_order": [ - "registrant_id", - "email", - "phone", - "column_break_ejwm", - "webinar", - "first_name", - "last_name", - "section_break_qhai", - "custom_question", - "amended_from" - ], - "fields": [ - { - "fieldname": "registrant_id", - "fieldtype": "Data", - "label": "Registrant ID" - }, - { - "fieldname": "email", - "fieldtype": "Data", - "label": "Email", - "options": "Email" - }, - { - "fieldname": "phone", - "fieldtype": "Data", - "label": "Phone" - }, - { - "fieldname": "column_break_ejwm", - "fieldtype": "Column Break" - }, - { - "fieldname": "webinar", - "fieldtype": "Link", - "label": "Webinar", - "options": "Zoom Webinar" - }, - { - "fieldname": "first_name", - "fieldtype": "Data", - "label": "First Name" - }, - { - "fieldname": "last_name", - "fieldtype": "Data", - "label": "Last Name" - }, - { - "fieldname": "section_break_qhai", - "fieldtype": "Section Break" - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Zoom Webinar Registrant Record", - "print_hide": 1, - "read_only": 1, - "search_index": 1 - }, - { - "fieldname": "custom_question", - "fieldtype": "Table", - "label": "Custom Question", - "options": "Zoom Webinar Additional Param" - } - ], - "grid_page_length": 50, - "index_web_pages_for_search": 1, - "is_submittable": 1, - "links": [], - "modified": "2025-11-16 18:36:58.319556", - "modified_by": "Administrator", - "module": "Zoom Integration", - "name": "Zoom Webinar Registrant Record", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "rows_threshold_for_grid_search": 20, - "sort_field": "creation", - "sort_order": "DESC", - "states": [] -} diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.py b/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.py deleted file mode 100644 index f2f7a4e..0000000 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/zoom_webinar_registrant_record.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2025, Build With Hussain and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class ZoomWebinarRegistrantRecord(Document): - # begin: auto-generated types - # This code is auto-generated. Do not modify anything in this block. - - from typing import TYPE_CHECKING - - if TYPE_CHECKING: - from frappe.types import DF - - from zoom_integration.zoom_integration.doctype.zoom_webinar_additional_param.zoom_webinar_additional_param import ( - ZoomWebinarAdditionalParam, - ) - - amended_from: DF.Link | None - custom_question: DF.Table[ZoomWebinarAdditionalParam] - email: DF.Data | None - first_name: DF.Data | None - last_name: DF.Data | None - phone: DF.Data | None - registrant_id: DF.Data | None - webinar: DF.Link | None - # end: auto-generated types - - pass diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar_registration/zoom_webinar_registration.json b/zoom_integration/zoom_integration/doctype/zoom_webinar_registration/zoom_webinar_registration.json index cadd46f..fe0d66c 100644 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar_registration/zoom_webinar_registration.json +++ b/zoom_integration/zoom_integration/doctype/zoom_webinar_registration/zoom_webinar_registration.json @@ -9,6 +9,12 @@ "webinar", "column_break_baez", "user", + "synced_from_zoom", + "attendee_details_section", + "email", + "column_break_tmmm", + "first_name", + "last_name", "section_break_rdgw", "additional_params", "zoom_information_section", @@ -73,7 +79,8 @@ "fieldname": "registrant_id", "fieldtype": "Data", "label": "Registrant ID", - "read_only": 1 + "read_only": 1, + "unique": 1 }, { "fieldname": "column_break_hzel", @@ -88,13 +95,53 @@ "fieldtype": "Table", "label": "Additional Params", "options": "Zoom Webinar Additional Param" + }, + { + "fetch_from": "user.email", + "fetch_if_empty": 1, + "fieldname": "email", + "fieldtype": "Data", + "label": "Email", + "options": "Email", + "read_only_depends_on": "eval:doc.user" + }, + { + "fieldname": "attendee_details_section", + "fieldtype": "Section Break", + "label": "Attendee Details" + }, + { + "fieldname": "column_break_tmmm", + "fieldtype": "Column Break" + }, + { + "fetch_from": "user.first_name", + "fetch_if_empty": 1, + "fieldname": "first_name", + "fieldtype": "Data", + "label": "First Name", + "read_only_depends_on": "eval:doc.user" + }, + { + "fetch_from": "user.last_name", + "fetch_if_empty": 1, + "fieldname": "last_name", + "fieldtype": "Data", + "label": "Last Name", + "read_only_depends_on": "eval:doc.user" + }, + { + "default": "0", + "fieldname": "synced_from_zoom", + "fieldtype": "Check", + "label": "Synced from Zoom?" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-10-31 12:45:50.275215", + "modified": "2025-12-09 13:13:19.795660", "modified_by": "Administrator", "module": "Zoom Integration", "name": "Zoom Webinar Registration", diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar_registration/zoom_webinar_registration.py b/zoom_integration/zoom_integration/doctype/zoom_webinar_registration/zoom_webinar_registration.py index b0133fd..467fec0 100644 --- a/zoom_integration/zoom_integration/doctype/zoom_webinar_registration/zoom_webinar_registration.py +++ b/zoom_integration/zoom_integration/doctype/zoom_webinar_registration/zoom_webinar_registration.py @@ -13,35 +13,47 @@ class ZoomWebinarRegistration(Document): if TYPE_CHECKING: from frappe.types import DF - - from zoom_integration.zoom_integration.doctype.zoom_webinar_additional_param.zoom_webinar_additional_param import ( - ZoomWebinarAdditionalParam, - ) + from zoom_integration.zoom_integration.doctype.zoom_webinar_additional_param.zoom_webinar_additional_param import ZoomWebinarAdditionalParam additional_params: DF.Table[ZoomWebinarAdditionalParam] amended_from: DF.Link | None + email: DF.Data | None + first_name: DF.Data | None join_url: DF.Data | None + last_name: DF.Data | None registrant_id: DF.Data | None + synced_from_zoom: DF.Check user: DF.Link | None webinar: DF.Link # end: auto-generated types def before_insert(self): - if not self.user: + if not (self.user or self.email): self.user = frappe.session.user + if self.email: + user_exists = frappe.db.exists("User", {"email": self.email}) + self.user = user_exists + if self.user == "Guest": frappe.throw("Guest user cannot register for webinar") + elif self.user and not self.email: + user_doc = frappe.get_cached_doc("User", self.user) + self.email = user_doc.email + self.first_name = user_doc.first_name + self.last_name = user_doc.last_name def before_submit(self): - user_doc = frappe.get_cached_doc("User", self.user) + if self.synced_from_zoom: + # this was already synced from zoom, no need to register again + return additional_params = {} if self.additional_params: additional_params = {param.key: param.value for param in self.additional_params} registration = frappe.get_cached_doc("Zoom Webinar", self.webinar).add_registrant( - user_doc.email, user_doc.first_name, user_doc.last_name, additional_params + self.email, self.first_name, self.last_name, additional_params ) self.join_url = registration.get("join_url") diff --git a/zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/__init__.py b/zoom_integration/zoom_integration/report/__init__.py similarity index 100% rename from zoom_integration/zoom_integration/doctype/zoom_webinar_registrant_record/__init__.py rename to zoom_integration/zoom_integration/report/__init__.py diff --git a/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/__init__.py b/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.js b/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.js new file mode 100644 index 0000000..8aa3bc9 --- /dev/null +++ b/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.js @@ -0,0 +1,13 @@ +// Copyright (c) 2025, Build With Hussain and contributors +// For license information, please see license.txt + +frappe.query_reports["Consolidated Webinar Attendance"] = { + filters: [ + // { + // "fieldname": "my_filter", + // "label": __("My Filter"), + // "fieldtype": "Data", + // "reqd": 1, + // }, + ], +}; diff --git a/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.json b/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.json new file mode 100644 index 0000000..d23c33d --- /dev/null +++ b/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.json @@ -0,0 +1,31 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2025-11-10 18:36:10.887906", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-11-10 18:36:18.175857", + "modified_by": "Administrator", + "module": "Zoom Integration", + "name": "Consolidated Webinar Attendance", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Zoom Webinar Attendance Record", + "report_name": "Consolidated Webinar Attendance", + "report_type": "Script Report", + "roles": [ + { + "role": "Zoom Manager" + }, + { + "role": "System Manager" + } + ], + "timeout": 0 +} diff --git a/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.py b/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.py new file mode 100644 index 0000000..90a197a --- /dev/null +++ b/zoom_integration/zoom_integration/report/consolidated_webinar_attendance/consolidated_webinar_attendance.py @@ -0,0 +1,48 @@ +# Copyright (c) 2025, Build With Hussain and contributors +# For license information, please see license.txt + +# import frappe +from frappe import _ + + +def execute(filters: dict | None = None): + """Return columns and data for the report. + + This is the main entry point for the report. It accepts the filters as a + dictionary and should return columns and data. It is called by the framework + every time the report is refreshed or a filter is updated. + """ + columns = get_columns() + data = get_data() + + return columns, data + + +def get_columns() -> list[dict]: + """Return columns for the report. + + One field definition per column, just like a DocType field definition. + """ + return [ + { + "label": _("Column 1"), + "fieldname": "column_1", + "fieldtype": "Data", + }, + { + "label": _("Column 2"), + "fieldname": "column_2", + "fieldtype": "Int", + }, + ] + + +def get_data() -> list[list]: + """Return data for the report. + + The report data is a list of rows, with each row being a list of cell values. + """ + return [ + ["Row 1", 1], + ["Row 2", 2], + ]