diff --git a/buzz/api/forms.py b/buzz/api/forms.py
index 77ee1cbd..d04c3268 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,55 @@ 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"),
+ "banner_title": settings.event_proposal_banner_title or _("Propose an Event"),
+ "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 f690cac4..22f23601 100644
--- a/buzz/events/doctype/buzz_settings/buzz_settings.json
+++ b/buzz/events/doctype/buzz_settings/buzz_settings.json
@@ -13,6 +13,15 @@
"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",
+ "event_proposal_banner_title",
+ "column_break_lxjh",
+ "allow_guest_event_proposals",
+ "success_section",
+ "event_proposal_success_title",
+ "event_proposal_success_message",
"login_tab",
"login_banner_section",
"login_banner",
@@ -76,6 +85,47 @@
"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_banner_title",
+ "fieldtype": "Data",
+ "label": "Banner Title"
+ },
+ {
+ "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": "login_tab",
"fieldtype": "Tab Break",
@@ -161,13 +211,23 @@
"fieldname": "custom_fields_go_after_this",
"fieldtype": "HTML",
"label": "Custom Fields Go After This"
+ },
+ {
+ "fieldname": "column_break_lxjh",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.accept_event_proposals",
+ "fieldname": "success_section",
+ "fieldtype": "Section Break",
+ "label": "Success"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2026-03-26 17:39:40.326309",
+ "modified": "2026-03-29 09:46:29.455686",
"modified_by": "Administrator",
"module": "Events",
"name": "Buzz Settings",
diff --git a/buzz/events/doctype/buzz_settings/buzz_settings.py b/buzz/events/doctype/buzz_settings/buzz_settings.py
index 3ab416c2..2902d212 100644
--- a/buzz/events/doctype/buzz_settings/buzz_settings.py
+++ b/buzz/events/doctype/buzz_settings/buzz_settings.py
@@ -15,7 +15,9 @@ class BuzzSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
+ accept_event_proposals: DF.Check
allow_add_ons_change_before_event_start_days: DF.Int
+ allow_guest_event_proposals: DF.Check
allow_ticket_cancellation_request_before_event_start_days: DF.Int
allow_transfer_ticket_before_event_start_days: DF.Int
auto_send_pitch_deck: DF.Check
@@ -23,6 +25,9 @@ class BuzzSettings(Document):
default_sponsor_deck_email_template: DF.Link | None
default_sponsor_deck_reply_to: DF.Data | None
default_ticket_email_template: DF.Link | None
+ event_proposal_banner_title: DF.Data | None
+ event_proposal_success_message: DF.MarkdownEditor | None
+ event_proposal_success_title: DF.Data | None
login_banner: DF.MarkdownEditor | None
support_email: DF.Data | None
# end: auto-generated types
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/components.d.ts b/dashboard/components.d.ts
index d8d060ca..59120500 100644
--- a/dashboard/components.d.ts
+++ b/dashboard/components.d.ts
@@ -28,6 +28,7 @@ declare module 'vue' {
LanguageSwitcher: typeof import('./src/components/LanguageSwitcher.vue')['default']
LoginDialog: typeof import('./src/components/LoginDialog.vue')['default']
LoginRequired: typeof import('./src/components/LoginRequired.vue')['default']
+ LucideAlertCircle: typeof import('~icons/lucide/alert-circle')['default']
Navbar: typeof import('./src/components/Navbar.vue')['default']
OfflinePaymentDialog: typeof import('./src/components/OfflinePaymentDialog.vue')['default']
PaymentGatewayDialog: typeof import('./src/components/PaymentGatewayDialog.vue')['default']
diff --git a/dashboard/src/components/BaseCustomEventForm.vue b/dashboard/src/components/BaseCustomEventForm.vue
index 642004f1..2b6fd73b 100644
--- a/dashboard/src/components/BaseCustomEventForm.vue
+++ b/dashboard/src/components/BaseCustomEventForm.vue
@@ -116,12 +116,12 @@
-
-
-
+
+
+
{{ __("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/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 @@
/>
+
+
+
+
+