You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Right now only admins can add calendar events — there's no way for a small-group leader or youth coordinator to propose one without admin keys. This adds a public /calendar/submit form (logged-in OR anonymous with captcha) that drops submissions into a pending queue an admin approves, mirroring the pattern we already shipped for Prayer Requests in PR #129.
Detailed proposal
Schema (additive, no breaking changes):
Add submissionStatus ENUM('pending','approved','rejected') NULL to tblEvents — NULL = legacy/admin-created (treat as approved), non-NULL = went through moderation
Add submittedBy INT NULL (FK tblUsers.userID, nullable for anonymous), submitterName VARCHAR(120) NULL, submitterEmail VARCHAR(255) NULL, submittedAt DATETIME NULL, moderatedBy INT NULL, moderatedAt DATETIME NULL, moderationNote TEXT NULL
New tblSettings keys: calendar.publicSubmit.enabled (bool), calendar.publicSubmit.allowAnonymous (bool), calendar.publicSubmit.requireCaptcha (bool, default true for anon), calendar.publicSubmit.notifySubmitter (bool), calendar.publicSubmit.allowedCategoryIDs (CSV — restrict which tblEventCategories rows are selectable)
Routes (register via AppRegistry, follow Prayer Requests precedent):
GET /calendar/submit — public form (brand-aware via Site::branding(), honours Site::productName() for copy)
GET /admin/calendar/moderation — pending queue (uses portal-data-list)
POST /admin/calendar/moderation/{eventID}/{approve|reject|edit} — on approve flip status='published', isPublic=1, submissionStatus='approved'; optionally email submitter
UI: form supports single events only in v1 (series/recurrence via tblEventSeries/tblEventRecurrence deferred — admins can promote on approval). RSVP (tblEventRsvps) and event-type (tblEventTypes) restricted to safe defaults; category picker filtered by allowedCategoryIDs.
Pros / Cons
Pros: unblocks ministry-level event entry without granting admin rights; reuses Prayer Requests moderation pattern, captcha stack, and AppRegistry plumbing already in production; high-leverage for church vertical (matches TEC Community Events parity); brand-aware out of the box; additive schema = zero migration risk.
Cons: new public attack surface (mitigated by captcha + moderation queue); adds admin workload unless allowedCategoryIDs is scoped tight; series/recurrence gap may frustrate power users in v1; needs clear UX to distinguish "submitted" vs "published" state in admin lists.
How necessary?
Medium-high for church installs (small-group/ministry leaders, youth coordinators); low for single-admin organisations.
Acceptance criteria
/calendar/submit route renders public form, honours calendar.publicSubmit.enabled kill-switch
ELI5
Right now only admins can add calendar events — there's no way for a small-group leader or youth coordinator to propose one without admin keys. This adds a public
/calendar/submitform (logged-in OR anonymous with captcha) that drops submissions into a pending queue an admin approves, mirroring the pattern we already shipped for Prayer Requests in PR #129.Detailed proposal
Schema (additive, no breaking changes):
submissionStatusENUM('pending','approved','rejected') NULL totblEvents— NULL = legacy/admin-created (treat as approved), non-NULL = went through moderationsubmittedByINT NULL (FKtblUsers.userID, nullable for anonymous),submitterNameVARCHAR(120) NULL,submitterEmailVARCHAR(255) NULL,submittedAtDATETIME NULL,moderatedByINT NULL,moderatedAtDATETIME NULL,moderationNoteTEXT NULLtblSettingskeys:calendar.publicSubmit.enabled(bool),calendar.publicSubmit.allowAnonymous(bool),calendar.publicSubmit.requireCaptcha(bool, default true for anon),calendar.publicSubmit.notifySubmitter(bool),calendar.publicSubmit.allowedCategoryIDs(CSV — restrict whichtblEventCategoriesrows are selectable)Routes (register via AppRegistry, follow Prayer Requests precedent):
GET /calendar/submit— public form (brand-aware viaSite::branding(), honoursSite::productName()for copy)POST /calendar/submit— CSRF + captcha (reuse multi-provider stack from PR feat(captcha): multi-provider support (Turnstile / reCAPTCHA v2+v3 / hCaptcha) with drag-and-drop priority #130) → insert withstatus='draft',isPublic=0,submissionStatus='pending'GET /admin/calendar/moderation— pending queue (usesportal-data-list)POST /admin/calendar/moderation/{eventID}/{approve|reject|edit}— on approve flipstatus='published',isPublic=1,submissionStatus='approved'; optionally email submitterUI: form supports single events only in v1 (series/recurrence via
tblEventSeries/tblEventRecurrencedeferred — admins can promote on approval). RSVP (tblEventRsvps) and event-type (tblEventTypes) restricted to safe defaults; category picker filtered byallowedCategoryIDs.Pros / Cons
Pros: unblocks ministry-level event entry without granting admin rights; reuses Prayer Requests moderation pattern, captcha stack, and
AppRegistryplumbing already in production; high-leverage for church vertical (matches TEC Community Events parity); brand-aware out of the box; additive schema = zero migration risk.Cons: new public attack surface (mitigated by captcha + moderation queue); adds admin workload unless
allowedCategoryIDsis scoped tight; series/recurrence gap may frustrate power users in v1; needs clear UX to distinguish "submitted" vs "published" state in admin lists.How necessary?
Medium-high for church installs (small-group/ministry leaders, youth coordinators); low for single-admin organisations.
Acceptance criteria
/calendar/submitroute renders public form, honourscalendar.publicSubmit.enabledkill-switchsubmissionStatus='pending',isPublic=0and do NOT appear in public calendar views/admin/calendar/moderationlists pending items with approve / reject / edit-then-publish actionsmoderationNoteEstimated effort
6-8 hours focused work (form + moderation page + settings UI + tests); pattern is well-trodden from Prayer Requests.
Filed during The Events Calendar competitive analysis on 2026-06-16; decision pending.