From da0492bf54db8d82b154d5112d22d4396d08e251 Mon Sep 17 00:00:00 2001
From: Rahul Agrawal <12agrawalrahul@gmail.com>
Date: Thu, 26 Mar 2026 16:30:04 +0530
Subject: [PATCH 1/3] feat: add option to publish event proposal form
---
buzz/api/forms.py | 62 +++++++++
.../doctype/buzz_settings/buzz_settings.json | 41 ++++++
.../doctype/event_proposal/event_proposal.py | 28 +++-
dashboard/src/components/CustomFieldInput.vue | 13 ++
dashboard/src/pages/EventProposalForm.vue | 130 ++++++++++++++++++
dashboard/src/router.ts | 6 +
plans/Completed/event-proposal-form.md | 108 +++++++++++++++
7 files changed, 384 insertions(+), 4 deletions(-)
create mode 100644 dashboard/src/pages/EventProposalForm.vue
create mode 100644 plans/Completed/event-proposal-form.md
diff --git a/buzz/api/forms.py b/buzz/api/forms.py
index 77ee1cbd..9880cb27 100644
--- a/buzz/api/forms.py
+++ b/buzz/api/forms.py
@@ -9,6 +9,17 @@
LAYOUT_FIELDTYPES = set(display_fieldtypes)
+EVENT_PROPOSAL_EXCLUDE_FIELDS = DEFAULT_FIELDS | {
+ "naming_series",
+ "amended_from",
+ "host",
+ "host_company",
+ "host_company_logo",
+ "additional_notes",
+ "status",
+ "submitted_by",
+}
+
STANDARD_EXCLUDE_FIELDS = DEFAULT_FIELDS | {
"additional_fields",
"event",
@@ -279,3 +290,54 @@ def submit_custom_form(
)
doc.insert(ignore_permissions=True)
+
+
+def validate_event_proposal_settings():
+ settings = frappe.get_cached_doc("Buzz Settings")
+ if not settings.accept_event_proposals:
+ frappe.throw(_("Event Proposals are not being accepted"), frappe.DoesNotExistError)
+
+ if not settings.allow_guest_event_proposals and frappe.session.user == "Guest":
+ frappe.throw(_("Please log in to submit a proposal"), frappe.AuthenticationError)
+
+ return settings
+
+
+@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
+def get_event_proposal_form_data() -> dict:
+ settings = validate_event_proposal_settings()
+ form_fields = get_form_fields("Event Proposal", EVENT_PROPOSAL_EXCLUDE_FIELDS)
+
+ return {
+ "form_fields": form_fields,
+ "form_title": _("Event Proposal"),
+ "success_title": settings.event_proposal_success_title or _("Thank you!"),
+ "success_message": settings.event_proposal_success_message or "",
+ }
+
+
+@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
+def submit_event_proposal(data: dict | str) -> None:
+ validate_event_proposal_settings()
+
+ data = frappe.parse_json(data) or {}
+
+ doc_data = {"doctype": "Event Proposal"}
+
+ if frappe.session.user != "Guest":
+ doc_data["submitted_by"] = frappe.session.user
+
+ allowed_fieldnames = {
+ f["fieldname"] for f in get_form_fields("Event Proposal", EVENT_PROPOSAL_EXCLUDE_FIELDS)
+ }
+ for fieldname, value in data.items():
+ if fieldname in allowed_fieldnames:
+ doc_data[fieldname] = value
+
+ meta = frappe.get_meta("Event Proposal")
+ for df in meta.fields:
+ if df.fieldtype == "Table" and df.fieldname not in EVENT_PROPOSAL_EXCLUDE_FIELDS:
+ if df.fieldname in data and isinstance(data[df.fieldname], list):
+ doc_data[df.fieldname] = data[df.fieldname]
+
+ frappe.get_doc(doc_data).insert(ignore_permissions=True)
diff --git a/buzz/events/doctype/buzz_settings/buzz_settings.json b/buzz/events/doctype/buzz_settings/buzz_settings.json
index 83cdc042..8020cbc9 100644
--- a/buzz/events/doctype/buzz_settings/buzz_settings.json
+++ b/buzz/events/doctype/buzz_settings/buzz_settings.json
@@ -13,6 +13,12 @@
"allow_add_ons_change_before_event_start_days",
"column_break_hagy",
"allow_ticket_cancellation_request_before_event_start_days",
+ "proposals_tab",
+ "event_proposals_section",
+ "accept_event_proposals",
+ "allow_guest_event_proposals",
+ "event_proposal_success_title",
+ "event_proposal_success_message",
"communications_tab",
"ticketing_emails_section",
"default_ticket_email_template",
@@ -73,6 +79,41 @@
"label": "Support Email",
"options": "Email"
},
+ {
+ "fieldname": "proposals_tab",
+ "fieldtype": "Tab Break",
+ "label": "Proposals"
+ },
+ {
+ "fieldname": "event_proposals_section",
+ "fieldtype": "Section Break",
+ "label": "Event Proposals"
+ },
+ {
+ "default": "0",
+ "fieldname": "accept_event_proposals",
+ "fieldtype": "Check",
+ "label": "Accept Event Proposals"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.accept_event_proposals",
+ "fieldname": "allow_guest_event_proposals",
+ "fieldtype": "Check",
+ "label": "Allow Guest Submission"
+ },
+ {
+ "depends_on": "eval:doc.accept_event_proposals",
+ "fieldname": "event_proposal_success_title",
+ "fieldtype": "Data",
+ "label": "Success Title"
+ },
+ {
+ "depends_on": "eval:doc.accept_event_proposals",
+ "fieldname": "event_proposal_success_message",
+ "fieldtype": "Markdown Editor",
+ "label": "Success Message"
+ },
{
"fieldname": "communications_tab",
"fieldtype": "Tab Break",
diff --git a/buzz/proposals/doctype/event_proposal/event_proposal.py b/buzz/proposals/doctype/event_proposal/event_proposal.py
index bb50efcb..aaff3b8e 100644
--- a/buzz/proposals/doctype/event_proposal/event_proposal.py
+++ b/buzz/proposals/doctype/event_proposal/event_proposal.py
@@ -2,9 +2,10 @@
# For license information, please see license.txt
import frappe
+from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
-from frappe.utils.data import get_url_to_form
+from frappe.utils.data import get_url_to_form, getdate, today
class EventProposal(Document):
@@ -35,9 +36,28 @@ class EventProposal(Document):
title: DF.Data
# end: auto-generated types
+ def validate(self):
+ self.validate_dates()
+ self.validate_times()
+
+ def validate_dates(self):
+ if getdate(self.start_date) < getdate(today()):
+ frappe.throw(_("Start Date cannot be in the past."))
+
+ if self.end_date and getdate(self.end_date) < getdate(self.start_date):
+ frappe.throw(_("End Date cannot be before Start Date."))
+
+ def validate_times(self):
+ if not self.start_time or not self.end_time:
+ return
+
+ same_day = not self.end_date or getdate(self.end_date) == getdate(self.start_date)
+ if same_day and self.end_time <= self.start_time:
+ frappe.throw(_("End Time must be after Start Time for same-day events."))
+
def before_submit(self):
if self.status not in ("Approved", "Rejected"):
- frappe.throw(frappe._("Only Approved or Rejected proposals can be submitted."))
+ frappe.throw(_("Only Approved or Rejected proposals can be submitted."))
self.create_event()
@@ -46,7 +66,7 @@ def create_event(self):
return
if not self.host:
- frappe.throw(frappe._("Please create or set a Host before submitting the proposal."))
+ frappe.throw(_("Please create or set a Host before submitting the proposal."))
buzz_event = get_mapped_doc(
"Event Proposal", self.name, {"Event Proposal": {"doctype": "Buzz Event"}}
@@ -57,7 +77,7 @@ def create_event(self):
self.status = "Event Created"
frappe.msgprint(
- frappe._("Buzz Event {0} created successfully.").format(
+ _("Buzz Event {0} created successfully.").format(
f'{buzz_event.title}'
)
)
diff --git a/dashboard/src/components/CustomFieldInput.vue b/dashboard/src/components/CustomFieldInput.vue
index cfc8545a..fe287733 100644
--- a/dashboard/src/components/CustomFieldInput.vue
+++ b/dashboard/src/components/CustomFieldInput.vue
@@ -23,6 +23,18 @@
/>
+
+
+
+
+
-
-
-
+
+
+
{{ __("Not Found") }}
-
+
{{ loadError }}
@@ -163,7 +163,6 @@ import { marked } from "marked";
import { computed, reactive, ref } from "vue";
import LucideAlertCircle from "~icons/lucide/alert-circle";
import LucideCheckCircle from "~icons/lucide/check-circle";
-import LucideXCircle from "~icons/lucide/x-circle";
const props = defineProps({
eventRoute: {
diff --git a/dashboard/src/pages/EventProposalForm.vue b/dashboard/src/pages/EventProposalForm.vue
index 7bdb5fc7..4cd198b5 100644
--- a/dashboard/src/pages/EventProposalForm.vue
+++ b/dashboard/src/pages/EventProposalForm.vue
@@ -28,14 +28,16 @@