From c223b6b918f01f0a4e020d7984454638d4b65c53 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Mon, 29 Jun 2026 11:35:13 +0530 Subject: [PATCH 1/3] feat: add Create Host button on Event Proposal Adds a Desk custom button that creates an Event Host from the proposal's host details (company name, logo, about) and auto-links it, mirroring the Create Sponsor flow. Reuses an existing host if one matches the company name. Closes #233 --- .../doctype/event_proposal/event_proposal.js | 9 +++ .../doctype/event_proposal/event_proposal.py | 23 +++++++ .../event_proposal/test_event_proposal.py | 60 ++++++++++++++++++- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/buzz/proposals/doctype/event_proposal/event_proposal.js b/buzz/proposals/doctype/event_proposal/event_proposal.js index aa70e915..2ecbd086 100644 --- a/buzz/proposals/doctype/event_proposal/event_proposal.js +++ b/buzz/proposals/doctype/event_proposal/event_proposal.js @@ -6,5 +6,14 @@ frappe.ui.form.on("Event Proposal", { if (!frm.is_new() && frm.doc.docstatus == 0) { frm.set_intro("Buzz Event will be created on submission of this document", "yellow"); } + + if (!frm.is_new() && frm.doc.docstatus == 0 && !frm.doc.host) { + frm.add_custom_button(__("Create Host"), () => { + frm.call("create_host").then(() => { + frappe.show_alert(__("Host Created!")); + frm.refresh(); + }); + }); + } }, }); diff --git a/buzz/proposals/doctype/event_proposal/event_proposal.py b/buzz/proposals/doctype/event_proposal/event_proposal.py index 425a3bcf..d5bde648 100644 --- a/buzz/proposals/doctype/event_proposal/event_proposal.py +++ b/buzz/proposals/doctype/event_proposal/event_proposal.py @@ -63,6 +63,29 @@ def before_submit(self): self.create_event() + @frappe.whitelist() + def create_host(self): + self.check_permission("write") + + if self.host: + frappe.throw(_("A Host is already linked to this proposal.")) + + if not self.host_company: + frappe.throw(_("Please enter the Company Name before creating a Host.")) + + if frappe.db.exists("Event Host", self.host_company): + self.host = self.host_company + else: + host = frappe.new_doc("Event Host") + host.name = self.host_company + host.logo = self.host_company_logo + host.about = self.about_the_company + host.insert(ignore_permissions=True) + self.host = host.name + + self.save() + return self.host + def create_event(self): if self.status == "Rejected": return diff --git a/buzz/proposals/doctype/event_proposal/test_event_proposal.py b/buzz/proposals/doctype/event_proposal/test_event_proposal.py index 6eecb995..9cb8b503 100644 --- a/buzz/proposals/doctype/event_proposal/test_event_proposal.py +++ b/buzz/proposals/doctype/event_proposal/test_event_proposal.py @@ -1,7 +1,7 @@ # 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 @@ -17,4 +17,60 @@ class IntegrationTestEventProposal(IntegrationTestCase): Use this class for testing interactions between multiple components. """ - pass + @classmethod + def setUpClass(cls): + super().setUpClass() + if not frappe.db.exists("Event Category", "Test Proposal Category"): + category = frappe.new_doc("Event Category") + category.name = "Test Proposal Category" + category.insert(ignore_permissions=True) + cls.category = "Test Proposal Category" + + def make_proposal(self, **kwargs): + proposal = frappe.new_doc("Event Proposal") + proposal.update( + { + "title": f"Proposal {frappe.generate_hash(length=6)}", + "category": self.category, + "medium": "Online", + "start_date": "2030-01-01", + "about": "
About the event
", + } + ) + proposal.update(kwargs) + proposal.insert(ignore_permissions=True) + return proposal + + def test_create_host_creates_and_links_event_host(self): + company = f"Acme {frappe.generate_hash(length=6)}" + proposal = self.make_proposal(host_company=company, about_the_company="We host events.") + + host_name = proposal.create_host() + + self.assertEqual(host_name, company) + self.assertEqual(proposal.host, company) + self.assertTrue(frappe.db.exists("Event Host", company)) + self.assertEqual(frappe.db.get_value("Event Host", company, "about"), "We host events.") + + def test_create_host_reuses_existing_host(self): + company = f"Existing {frappe.generate_hash(length=6)}" + existing = frappe.new_doc("Event Host") + existing.name = company + existing.insert(ignore_permissions=True) + + proposal = self.make_proposal(host_company=company) + proposal.create_host() + + self.assertEqual(proposal.host, company) + + def test_create_host_requires_company_name(self): + proposal = self.make_proposal() + with self.assertRaises(frappe.ValidationError): + proposal.create_host() + + def test_create_host_throws_if_already_linked(self): + company = f"Acme {frappe.generate_hash(length=6)}" + proposal = self.make_proposal(host_company=company) + proposal.create_host() + with self.assertRaises(frappe.ValidationError): + proposal.create_host() From 91af92ded635e876a93669bdc4ea3d2553e7737b Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Mon, 29 Jun 2026 13:04:53 +0530 Subject: [PATCH 2/3] feat: auto-create host on proposal submit when none is linked If a proposal is submitted without a linked host but has a company name, the Event Host is created and linked automatically instead of erroring. The Create Host button remains for doing it earlier. --- .../doctype/event_proposal/event_proposal.py | 15 +++++++++++---- .../event_proposal/test_event_proposal.py | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/buzz/proposals/doctype/event_proposal/event_proposal.py b/buzz/proposals/doctype/event_proposal/event_proposal.py index d5bde648..8a183579 100644 --- a/buzz/proposals/doctype/event_proposal/event_proposal.py +++ b/buzz/proposals/doctype/event_proposal/event_proposal.py @@ -70,6 +70,11 @@ def create_host(self): if self.host: frappe.throw(_("A Host is already linked to this proposal.")) + self._create_host() + self.save() + return self.host + + def _create_host(self): if not self.host_company: frappe.throw(_("Please enter the Company Name before creating a Host.")) @@ -83,20 +88,22 @@ def create_host(self): host.insert(ignore_permissions=True) self.host = host.name - self.save() - return self.host - def create_event(self): if self.status == "Rejected": return if not self.host: - frappe.throw(_("Please create or set a Host before submitting the proposal.")) + if not self.host_company: + frappe.throw(_("Please set a Host (or enter a Company Name) before submitting.")) + self._create_host() buzz_event = get_mapped_doc( "Event Proposal", self.name, {"Event Proposal": {"doctype": "Buzz Event"}} ) buzz_event.proposal = self.name + # host may have just been auto-created in-memory and is not yet persisted, + # so the mapped doc (read from DB) would miss it. + buzz_event.host = self.host buzz_event.insert() self.status = "Event Created" diff --git a/buzz/proposals/doctype/event_proposal/test_event_proposal.py b/buzz/proposals/doctype/event_proposal/test_event_proposal.py index 9cb8b503..e24c4920 100644 --- a/buzz/proposals/doctype/event_proposal/test_event_proposal.py +++ b/buzz/proposals/doctype/event_proposal/test_event_proposal.py @@ -34,6 +34,8 @@ def make_proposal(self, **kwargs): "category": self.category, "medium": "Online", "start_date": "2030-01-01", + "start_time": "10:00:00", + "end_time": "18:00:00", "about": "About the event
", } ) @@ -74,3 +76,18 @@ def test_create_host_throws_if_already_linked(self): proposal.create_host() with self.assertRaises(frappe.ValidationError): proposal.create_host() + + def test_submit_auto_creates_host_from_company(self): + company = f"Auto {frappe.generate_hash(length=6)}" + proposal = self.make_proposal(host_company=company, status="Approved") + + proposal.submit() + + self.assertEqual(proposal.host, company) + self.assertTrue(frappe.db.exists("Event Host", company)) + self.assertEqual(proposal.status, "Event Created") + + def test_submit_without_host_or_company_throws(self): + proposal = self.make_proposal(status="Approved") + with self.assertRaises(frappe.ValidationError): + proposal.submit() From 3c6180886ff4fa9c40cc052822dc688e669a6281 Mon Sep 17 00:00:00 2001 From: Rahul Agrawal <12agrawalrahul@gmail.com> Date: Mon, 29 Jun 2026 13:14:44 +0530 Subject: [PATCH 3/3] feat: enrich reused host with proposal logo/about when blank When Create Host links an existing Event Host, fill its logo/about from the proposal only where the host's fields are empty (never overwrite). --- .../doctype/event_proposal/event_proposal.py | 14 ++++++- .../event_proposal/test_event_proposal.py | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/buzz/proposals/doctype/event_proposal/event_proposal.py b/buzz/proposals/doctype/event_proposal/event_proposal.py index 8a183579..adb9f261 100644 --- a/buzz/proposals/doctype/event_proposal/event_proposal.py +++ b/buzz/proposals/doctype/event_proposal/event_proposal.py @@ -79,14 +79,24 @@ def _create_host(self): frappe.throw(_("Please enter the Company Name before creating a Host.")) if frappe.db.exists("Event Host", self.host_company): - self.host = self.host_company + host = frappe.get_doc("Event Host", self.host_company) + updated = False + if self.host_company_logo and not host.logo: + host.logo = self.host_company_logo + updated = True + if self.about_the_company and not host.about: + host.about = self.about_the_company + updated = True + if updated: + host.save(ignore_permissions=True) else: host = frappe.new_doc("Event Host") host.name = self.host_company host.logo = self.host_company_logo host.about = self.about_the_company host.insert(ignore_permissions=True) - self.host = host.name + + self.host = host.name def create_event(self): if self.status == "Rejected": diff --git a/buzz/proposals/doctype/event_proposal/test_event_proposal.py b/buzz/proposals/doctype/event_proposal/test_event_proposal.py index e24c4920..09362ca8 100644 --- a/buzz/proposals/doctype/event_proposal/test_event_proposal.py +++ b/buzz/proposals/doctype/event_proposal/test_event_proposal.py @@ -65,6 +65,45 @@ def test_create_host_reuses_existing_host(self): self.assertEqual(proposal.host, company) + def test_reuse_fills_only_empty_host_fields(self): + # Existing host with empty logo/about -> proposal values fill them in. + company = f"Empty {frappe.generate_hash(length=6)}" + frappe.get_doc({"doctype": "Event Host", "__newname": company}).insert(ignore_permissions=True) + + proposal = self.make_proposal( + host_company=company, + host_company_logo="/files/proposal-logo.png", + about_the_company="Proposal about.", + ) + proposal.create_host() + + host = frappe.get_doc("Event Host", company) + self.assertEqual(host.logo, "/files/proposal-logo.png") + self.assertEqual(host.about, "Proposal about.") + + def test_reuse_does_not_overwrite_populated_host_fields(self): + # Existing host already has logo/about -> proposal must not clobber them. + company = f"Populated {frappe.generate_hash(length=6)}" + frappe.get_doc( + { + "doctype": "Event Host", + "__newname": company, + "logo": "/files/original-logo.png", + "about": "Original about.", + } + ).insert(ignore_permissions=True) + + proposal = self.make_proposal( + host_company=company, + host_company_logo="/files/proposal-logo.png", + about_the_company="Proposal about.", + ) + proposal.create_host() + + host = frappe.get_doc("Event Host", company) + self.assertEqual(host.logo, "/files/original-logo.png") + self.assertEqual(host.about, "Original about.") + def test_create_host_requires_company_name(self): proposal = self.make_proposal() with self.assertRaises(frappe.ValidationError):