feat(dashboard): create an event from the dashboard - #354
Conversation
Greptile SummaryThe PR adds dashboard event creation, including scheduling, venue creation, Zoom selection, generated banners, role-aware UI controls, and server-side event defaults. It also adds event-level venue ownership validation, but the cross-team disclosure fix remains incomplete for unstamped venues.
Confidence Score: 3/5The PR is not yet safe to merge because the attempted venue-ownership fix still permits cross-team address disclosure through unstamped venues. The new validator rejects venues belonging to a different populated team but deliberately accepts null or empty team values; the create API can therefore persist one of those venues on an event, and booking and calendar paths subsequently expose its address without another ownership check. Files Needing Attention: buzz/events/doctype/buzz_event/buzz_event.py and buzz/api/events/services.py
|
| Filename | Overview |
|---|---|
| buzz/api/events/services.py | Adds authorized event creation, derived host/category values, and optional Zoom booking; the caller-provided venue relies on Buzz Event validation for ownership enforcement. |
| buzz/events/doctype/buzz_event/buzz_event.py | Adds venue-team validation, but accepts null or empty venue teams and therefore leaves the previously reported disclosure reachable. |
| dashboard/src/pages/manage/events/CreateEvent.vue | Adds the event-creation form and correctly prevents roles below Manager from submitting through canCreate and canSave. |
| dashboard/src/composables/useTeamAccess.ts | Adds a tri-state membership guard for pages outside the manager shell; role-specific creation is separately enforced by the page. |
| dashboard/src/router.ts | Registers the full-bleed create-event route outside the manager shell, with access handled by the page guard. |
| dashboard/src/components/dashboard/events/EventLocation.vue | Adds a unified venue and Zoom selector with inline venue creation. |
| dashboard/src/components/dashboard/events/EventSchedule.vue | Adds event date, time, and timezone controls for the creation flow. |
Prompt To Fix All With AI
### Issue 1
buzz/events/doctype/buzz_event/buzz_event.py:115-118
**Unstamped venues bypass ownership**
When a manager supplies the name of a legacy venue whose `team` is null or empty, this truthiness check accepts the venue and links it to the manager's event, causing its address to be exposed through booking details, booking confirmation, and calendar invitations.
**How this was verified:** The create API copies the supplied venue after authorizing only the event team, while downstream booking and calendar paths dereference the accepted venue without another ownership check.
```suggestion
venue_team = frappe.db.get_value("Event Venue", self.venue, "team")
if venue_team != self.team:
frappe.throw(_("Venue {0} belongs to another team.").format(self.venue))
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (8): Last reviewed commit: "feat(dashboard): keep an event from endi..." | Re-trigger Greptile
259edce to
ff14afc
Compare
b158200 to
e28b2a2
Compare
ff14afc to
b23702f
Compare
e28b2a2 to
76c03ac
Compare
b23702f to
5ec7c74
Compare
76c03ac to
45a5841
Compare
|
✅ UI Demo Check — a screenshot or demo is attached. |
45a5841 to
a42088d
Compare
94f451c to
3a81380
Compare
`Event Venue`, `Event Host` and `Buzz Event` all gained a mandatory team when multi-tenancy landed, but the records `before_tests` seeds were not updated. The seeding runs before any test does, so `bench run-tests` has been failing for every module rather than for one. Seeds them with the Administrator's default team, which the same function already creates for exactly this reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The manager shell resolves the team named in the route and 404s a team the user does not belong to. A full-screen page has the same need but cannot be a child of the layout that holds the logic, so the watcher moves into a composable both can call. No behaviour change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds `buzz.api.events.create_event`, the endpoint behind the dashboard's create form. Membership decides who may use it: anything below Manager on the team is refused, as is a non-member. `Buzz Event` requires a category and a host that the form does not ask for, so the service fills them in. The host is the team's own, minted on first use since a new team has none. The category is Meetups, or Zoom Meeting when a Zoom meeting was asked for — that one is not cosmetic, it is what marks an event as Zoom-backed. The Zoom meeting is booked after the event exists and linked back to it, the same call the Desk button makes. Zoom is checked before the insert so a missing integration refuses outright rather than half-making an event, and a failure during the call leaves the event standing rather than rolling it back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The contour rings were dense enough to moire at card size, and at one pixel they broke into a dotted stipple wherever a curve ran near-horizontal. Widens the spacing and draws the rings two pixels thick, so they read as lines. The line width becomes a constant rather than a literal repeated across both gradient stops — the test that pins ring spacing was matching on it by accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One control for both kinds of location: the team's venues, searched as you type, and a Zoom meeting under Virtual. A name nothing answers to offers to add a venue, carrying what was typed into the dialog. Venues read and write through `frappe.client`; Event Venue's team query condition and permission hook are what scope them, so the team filter is only narrowing what the picker asks for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Start and end, each a date beside a time, and a time zone picker searchable by city, country, everyday name or zone id. The zone data all comes from Intl, so the list tracks the browser's own tzdata with nothing to maintain. Country needs inverting: there is no zone-to-country lookup, only region-to-zones, so every two-letter code is swept once, on first open rather than on mount. Zones IANA has renamed still arrive under the old name, and a picker offering "Calcutta" to an organiser in India is wrong, so the ones a reader would notice are mapped forward. Dates are constrained rather than validated after the fact: the start cannot be in the past, the end cannot precede the start, and an end that has fallen behind follows the start rather than a multi-day span collapsing to one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reached from the Create Event button on the team pages, at /manage/<team>/events/new. Full screen rather than inside the manager shell, so the team comes from the route guard rather than the sidebar. The page is the event as it will look: a banner, the title as the headline, and the rest arranged around them. Without an image the banner draws the same generated pattern the event cards use, seeded by the title as it is typed — debounced, because a gradient cannot be transitioned, and blurred across the change, because that is the only way to hide one. An event needs a name, when it runs and where before it can be created; the description is optional. Uploads are restricted to raster images, both in the picker and on the file that comes back, and a rejected file now says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a42088d to
f8b03af
Compare
Event Venue is autonamed by prompt, so a venue's docname is its human name and therefore guessable. Nothing checked that the venue an event links to belongs to the event's own team, and nothing downstream re-checks it: the booking confirmation and the calendar invite both read the linked venue's address without a permission check. A manager could name another team's venue and publish its address. The guard goes on Buzz Event rather than on the create endpoint, so it covers the Desk form and the Event Template clone path too. An unstamped venue predates the team backfill and is left alone, matching what has_team_access does with an unstamped row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The create page only checked team membership, so a Viewer reached the full form and lost the work to a 403 on save. The server is the authority and already refuses anything below Manager; this just stops the form pretending otherwise. An alert sits under the header and every control is disabled. The pickers take frappe-ui's own disabled prop, so the greying comes from the design system. Three controls need more than that: the timezone combobox supplies its own trigger, which does not inherit the disabled state; the editor is contenteditable, so it takes editable instead; and the banner's hit area is a div, which no disabled prop reaches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Buzz Event already refuses this on save, but the form gave no sign until the round trip came back. Two layers now, because neither covers the other. The end time picker takes a min of the start time, which bounds the list it offers and reverts a typed value below it. That only applies to a single-day event: a span running into another day may well end earlier in the day than it began, so the bound lifts once the end date differs. The picker's min cannot cover everything, so the check stays alongside it. Min is inclusive, which still allows an end landing exactly on the start, and it only constrains new input — an end time picked while the event was multi-day is left untouched when the end date is pulled back onto the start date. Both show the same message under the End row and hold the save. Also renders the server's own validation errors, which arrive as `messages` rather than a single `message` and so came through blank. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| venue_team = frappe.db.get_value("Event Venue", self.venue, "team") | ||
| # An unstamped venue predates the team backfill; role permissions still gate it. | ||
| if venue_team and venue_team != self.team: | ||
| frappe.throw(_("Venue {0} belongs to another team.").format(self.venue)) |
There was a problem hiding this comment.
Unstamped venues bypass ownership
When a manager supplies the name of a legacy venue whose team is null or empty, this truthiness check accepts the venue and links it to the manager's event, causing its address to be exposed through booking details, booking confirmation, and calendar invitations.
How this was verified: The create API copies the supplied venue after authorizing only the event team, while downstream booking and calendar paths dereference the accepted venue without another ownership check.
| venue_team = frappe.db.get_value("Event Venue", self.venue, "team") | |
| # An unstamped venue predates the team backfill; role permissions still gate it. | |
| if venue_team and venue_team != self.team: | |
| frappe.throw(_("Venue {0} belongs to another team.").format(self.venue)) | |
| venue_team = frappe.db.get_value("Event Venue", self.venue, "team") | |
| if venue_team != self.team: | |
| frappe.throw(_("Venue {0} belongs to another team.").format(self.venue)) |
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: buzz/events/doctype/buzz_event/buzz_event.py
Line: 115-118
Comment:
**Unstamped venues bypass ownership**
When a manager supplies the name of a legacy venue whose `team` is null or empty, this truthiness check accepts the venue and links it to the manager's event, causing its address to be exposed through booking details, booking confirmation, and calendar invitations.
**How this was verified:** The create API copies the supplied venue after authorizing only the event team, while downstream booking and calendar paths dereference the accepted venue without another ownership check.
```suggestion
venue_team = frappe.db.get_value("Event Venue", self.venue, "team")
if venue_team != self.team:
frappe.throw(_("Venue {0} belongs to another team.").format(self.venue))
```
**Knowledge Base Used:**
- [Event configuration and scheduling](https://app.greptile.com/bwh-tech/-/custom-context/knowledge-base/bwhtech/buzz/-/docs/event-configuration.md)
- [Frappe application integration](https://app.greptile.com/bwh-tech/-/custom-context/knowledge-base/bwhtech/buzz/-/docs/frappe-application-integration.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Valid catch — an unstamped venue is linkable by any team, which is the same leak narrowed to legacy rows. The justification in my comment was a bad analogy: has_team_access abstains on an unstamped row because role permissions still gate it, and nothing gates a venue link.
Not taking the suggestion in this PR, though. Dropping the truthiness check turns a data problem into a save failure — any site still holding such a row would find those events unsaveable, including edits that never touch the venue. The backfill should land first, then the guard tightens.
Tracked in #375, which also notes that buzz.patches.assign_default_team already covers Event Venue and is logged as applied, so the remaining work is mostly confirming it left nothing behind before the guard changes.
There was a problem hiding this comment.
That’s a reasonable trade-off for this PR. The current check still protects already-stamped venues, while tightening it before the backfill is verified would make legacy records unsaveable—even on unrelated edits. #375 is the right follow-up: confirm assign_default_team left no unstamped venues, then change the guard to reject any venue whose team differs, including empty values.
Stacked on
develop.What changed
Creating an event needed Desk. This adds a create page at
/manage/team/events/new,reached from the Create Event button that has been inert on the team pages since they
shipped. It sits outside the manager shell, so the team comes from the route guard
rather than the layout.
buzz.api.events.create_event— membership decides who may call it; anythingbelow Manager is refused, as is a non-member.
city, country, everyday name or zone id.
with an inline add, or a Zoom meeting under Virtual.
up (wider spacing, 2px lines) since it moired at card size.
Worth a look during review:
Buzz Eventrequires both and theform asks for neither. Host is the team's own, minted on first use. Category is
Meetups, orZoom Meetingwhen Zoom was chosen — that one isn't cosmetic, it's whatmarks an event Zoom-backed (
buzz/utils.py:13).failure leaves the event standing rather than rolling it back, since the call goes out
to Zoom mid-request.
their old names (
Asia/Calcutta). The eight a reader would notice are mapped forward;both spellings resolve the same in
ZoneInfo.fix(tests): stamp a team on the seeded test recordsrides along.before_testsseeds a venue, host and event without the team those doctypes made mandatory in the
multi-tenancy work, so
bench run-testsfailed for every module before a singletest ran. Nothing here could be tested without it. Happy to split it out.
Not done: no unsaved-changes guard, so Back discards a filled-in form silently; nothing
in the dashboard has been checked in dark mode;
mediumis derived server-side only, sothe picker no longer tracks it.
Demo
The create page, filled in — the banner pattern is seeded by the title as you type:
One control for both kinds of location, venues and Zoom under the same picker:
Time zone search, by city rather than by zone id:

If the user is Viewer for the team
