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 @@ />

+
+ + +
+