Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
### Entry + Build
- Entry: `dashboard/src/main.js` mounts `App.vue` with router, resources, translation plugin, and socket.
- Build: `dashboard/vite.config.js` outputs to `buzz/public/dashboard` and updates `buzz/www/dashboard.html`.
- Base URL: `/dashboard` (router history uses `createWebHistory("/dashboard")`).
- Base URL: `/b` (router history uses `createWebHistory("/b")`). Old `/dashboard/*` links 301-redirect to `/b/*` via `website_redirects` in `hooks.py`.

### Routing
- Public-like flows: booking (`/book-tickets/:eventRoute`) and check-in (`/check-in`).
Expand Down Expand Up @@ -237,7 +237,7 @@ erDiagram
- Update frontend rendering in `CustomFieldInput.vue` and `BookingForm.vue`.
- Payment flow changes
- Update `buzz/payments.py` and any event-scoped gateway selection logic.
- Ensure payment redirects still land on `/dashboard/...?...success=true`.
- Ensure payment redirects still land on `/b/...?...success=true`.
- Sponsorship flow changes
- Update `Sponsorship Enquiry` for status transitions and `SponsorshipDetails.vue` for UI state.
- Verify sponsor creation in `on_payment_authorized`.
Expand Down
4 changes: 2 additions & 2 deletions buzz/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ def process_booking(
return {
"payment_link": get_payment_link_for_booking(
booking.name,
redirect_to=f"/dashboard/bookings/{booking.name}?success=true",
redirect_to=f"/b/bookings/{booking.name}?success=true",
payment_gateway=payment_gateway,
)
}
Expand Down Expand Up @@ -849,7 +849,7 @@ def create_sponsorship_payment_link(enquiry_id: str, tier_id: str, payment_gatew
if enquiry.owner != frappe.session.user:
frappe.throw(frappe._("Not permitted to create payment for this enquiry"))

redirect_url = f"/dashboard/account/sponsorships/{enquiry_id}?success=true"
redirect_url = f"/b/account/sponsorships/{enquiry_id}?success=true"
return get_payment_link_for_sponsorship(
enquiry_id, tier_id, redirect_url, payment_gateway=payment_gateway
)
Expand Down
2 changes: 1 addition & 1 deletion buzz/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def get_login_context(redirect_to: str | None = None):
}

if not redirect_to:
redirect_to = frappe.utils.get_url("/dashboard")
redirect_to = frappe.utils.get_url("/b")

social_login_keys = frappe.get_all(
"Social Login Key",
Expand Down
2 changes: 1 addition & 1 deletion buzz/buzz_marketing/doctype/buzz_campaign/buzz_campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def validate_crm_installed(self):
frappe.throw(_("Please install Frappe CRM to use campaigns feature"))

def generate_qr_code(self):
register_url = f"{frappe.utils.get_url()}/dashboard/register-interest/{self.name}"
register_url = f"{frappe.utils.get_url()}/b/register-interest/{self.name}"
self.qr_code = generate_qr_code_file(
doc=self,
data=register_url,
Expand Down
6 changes: 3 additions & 3 deletions buzz/events/doctype/buzz_event/buzz_event.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ function show_save_as_template_dialog(frm) {
frappe.ui.form.on("Buzz Event Form", {
copy_to_clipboard(frm, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const url = `${window.location.origin}/dashboard/events/${frm.doc.route}/forms/${row.route}`;
const url = `${window.location.origin}/b/${frm.doc.route}/${row.route}`;
navigator.clipboard.writeText(url);
frappe.show_alert({ message: __("Link copied!"), indicator: "green" });
},
Expand All @@ -307,11 +307,11 @@ frappe.ui.form.on("Buzz Event", {
}

if (frm.doc.route) {
frm.add_web_link(`/dashboard/book-tickets/${frm.doc.route}`, "View Registration Page");
frm.add_web_link(`/b/register/${frm.doc.route}`, "View Registration Page");
}

if (!frm.is_new()) {
frm.add_web_link(`/dashboard/check-in/${frm.doc.name}`, __("Open Check-in"));
frm.add_web_link(`/b/check-in/${frm.doc.name}`, __("Open Check-in"));
}

const button_label = frm.doc.is_published ? __("Unpublish") : __("Publish");
Expand Down
18 changes: 18 additions & 0 deletions buzz/events/doctype/buzz_event/buzz_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
from buzz.api.forms import validate_excluded_fields
from buzz.utils import only_if_app_installed

# Top-level dashboard route segments (/b/<segment>) an event route must not shadow.
RESERVED_EVENT_ROUTES = {
"account",
"bookings",
"tickets",
"register",
"register-interest",
"check-in",
"book-tickets",
"event-proposal",
"events",
}


class BuzzEvent(Document):
# begin: auto-generated types
Expand Down Expand Up @@ -145,6 +158,11 @@ def validate_route(self):
route = frappe.website.utils.cleanup_page_name(self.title).replace("_", "-")
self.route = append_number_if_name_exists("Buzz Event", route, fieldname="route")

if self.route in RESERVED_EVENT_ROUTES:
frappe.throw(
_("'{0}' is a reserved route and cannot be used as an event route.").format(self.route)
)

def validate_guest_verification_config(self):
"""Ensure email/SMS is configured when OTP verification is enabled."""
if frappe.in_test or not self.allow_guest_booking:
Expand Down
10 changes: 9 additions & 1 deletion buzz/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@


website_route_rules = [
{"from_route": "/dashboard/<path:app_path>", "to_route": "dashboard"},
{"from_route": "/b/<path:app_path>", "to_route": "dashboard"},
]

# Keep old /dashboard/* links working: redirect to the shortened /b/* scheme.
# Ordered specific -> catch-all; the first matching source wins.
website_redirects = [
{"source": r"/dashboard/events/([^/]+)/forms/([^/]+)", "target": r"/b/\1/\2"},
{"source": r"/dashboard/book-tickets/(.+)", "target": r"/b/register/\1"},
{"source": r"/dashboard/(.*)", "target": r"/b/\1"},
]

# Scheduled Tasks
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def send_pitch_deck(self, now=False):
def send_approval_notification(self):
event = frappe.get_cached_doc("Buzz Event", self.event)
host_name = event.host or "The Event Team"
dashboard_link = get_url(f"/dashboard/account/sponsorships/{self.name}")
dashboard_link = get_url(f"/b/account/sponsorships/{self.name}")

subject = f"[Payment Pending] Your Sponsorship for {event.title} has been Approved!"
message = f"""
Expand Down
2 changes: 1 addition & 1 deletion buzz/ticketing/doctype/event_ticket/event_ticket.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def send_user_invitation(self):
invite_by_email(
emails=self.attendee_email,
roles=["Buzz User"],
redirect_to_path="/dashboard/account/tickets",
redirect_to_path="/b/account/tickets",
app_name="buzz",
)

Expand Down
29 changes: 20 additions & 9 deletions dashboard/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
component: () => import("@/pages/CheckInScanner.vue"),
},
{
path: "/book-tickets/:eventRoute",
path: "/register/:eventRoute",
props: true,
name: "event-booking",
meta: { isPublic: true },
Expand All @@ -26,19 +26,21 @@ const routes: RouteRecordRaw[] = [
meta: { isPublic: true },
component: () => import("@/pages/EventProposalForm.vue"),
},
{
path: "/events/:eventRoute/forms/:formRoute",
props: true,
name: "custom-form",
meta: { isPublic: true },
component: () => import("@/pages/CustomFormPage.vue"),
},
{
path: "/register-interest/:campaign",
props: true,
name: "register-interest",
component: () => import("@/pages/RegisterInterest.vue"),
},
// Back-compat: old in-app paths redirect to the shortened scheme.
{
path: "/book-tickets/:eventRoute",
redirect: (to) => ({ name: "event-booking", params: to.params }),
},
{
path: "/events/:eventRoute/forms/:formRoute",
redirect: (to) => ({ name: "custom-form", params: to.params }),
},
{
path: "/bookings",
name: "bookings-tab",
Expand Down Expand Up @@ -113,10 +115,19 @@ const routes: RouteRecordRaw[] = [
},
],
},
// Event custom form: /b/<event>/<form>. Declared last — two dynamic segments,
// so it only matches after every static route above has been ruled out.
{
path: "/:eventRoute/:formRoute",
props: true,
name: "custom-form",
meta: { isPublic: true },
component: () => import("@/pages/CustomFormPage.vue"),
},
]

const router = createRouter({
history: createWebHistory("/dashboard"),
history: createWebHistory("/b"),
routes,
})

Expand Down
2 changes: 1 addition & 1 deletion e2e/pages/booking.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class BookingPage {

// Navigate to the booking page for a specific event.
async goto(eventRoute: string): Promise<void> {
await this.page.goto(`/dashboard/book-tickets/${eventRoute}`);
await this.page.goto(`/b/register/${eventRoute}`);
await this.page.waitForLoadState("networkidle");
}

Expand Down
2 changes: 1 addition & 1 deletion e2e/pages/custom-form.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class CustomFormPage {
}

async goto(eventRoute: string, formRoute: string): Promise<void> {
await this.page.goto(`/dashboard/events/${eventRoute}/forms/${formRoute}`);
await this.page.goto(`/b/${eventRoute}/${formRoute}`);
await this.page.waitForLoadState("networkidle");
}

Expand Down
2 changes: 1 addition & 1 deletion e2e/pages/event-proposal.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class EventProposalPage {
}

async goto(): Promise<void> {
await this.page.goto("/dashboard/event-proposal");
await this.page.goto("/b/event-proposal");
await this.page.waitForLoadState("networkidle");
}

Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { isLoggedIn } from "../helpers";
test.describe("Authentication - Pre-authenticated", () => {
test("should access buzz when authenticated", async ({ page }) => {
// Already authenticated via setup project
await page.goto("/dashboard/");
await page.goto("/b/");
await page.waitForLoadState("networkidle");

// Should not be redirected to login
Expand Down Expand Up @@ -51,7 +51,7 @@ test.describe("Authentication - Fresh state", () => {
});

test("should show login button when not authenticated", async ({ page }) => {
await page.goto("/dashboard");
await page.goto("/b");
await page.waitForLoadState("networkidle");

await expect(page.getByRole("button", { name: "Log In" }).first()).toBeVisible();
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/event.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,5 @@ setup("create test event for booking", async ({ request }) => {
});
console.log(`Created Ticket Add-on: ${addOn.name}`);

console.log(`Test event setup complete! Route: /dashboard/book-tickets/${testEventRoute}`);
console.log(`Test event setup complete! Route: /b/register/${testEventRoute}`);
});
4 changes: 2 additions & 2 deletions e2e/tests/login-modal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
test.use({ storageState: { cookies: [], origins: [] } });

test("email/password login via modal", async ({ page }) => {
await page.goto("/dashboard");
await page.goto("/b");
await page.waitForLoadState("networkidle");
await page.getByRole("button", { name: "Log In" }).first().click();

Check failure on line 12 in e2e/tests/login-modal.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright E2E Tests

[login-modal] › e2e/tests/login-modal.spec.ts:9:6 › Login Modal › email/password login via modal

1) [login-modal] › e2e/tests/login-modal.spec.ts:9:6 › Login Modal › email/password login via modal Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 15000ms exceeded. Call log: - waiting for getByRole('button', { name: 'Log In' }).first() 10 | await page.goto("/b"); 11 | await page.waitForLoadState("networkidle"); > 12 | await page.getByRole("button", { name: "Log In" }).first().click(); | ^ 13 | await expect(page.getByRole("dialog")).toBeVisible(); 14 | await expect(page.getByText("Login to Continue")).toBeVisible(); 15 | await page.getByLabel("Email").fill(FRAPPE_USER); at /home/runner/work/buzz/buzz/e2e/tests/login-modal.spec.ts:12:62

Check failure on line 12 in e2e/tests/login-modal.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright E2E Tests

[login-modal] › e2e/tests/login-modal.spec.ts:9:6 › Login Modal › email/password login via modal

1) [login-modal] › e2e/tests/login-modal.spec.ts:9:6 › Login Modal › email/password login via modal Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 15000ms exceeded. Call log: - waiting for getByRole('button', { name: 'Log In' }).first() 10 | await page.goto("/b"); 11 | await page.waitForLoadState("networkidle"); > 12 | await page.getByRole("button", { name: "Log In" }).first().click(); | ^ 13 | await expect(page.getByRole("dialog")).toBeVisible(); 14 | await expect(page.getByText("Login to Continue")).toBeVisible(); 15 | await page.getByLabel("Email").fill(FRAPPE_USER); at /home/runner/work/buzz/buzz/e2e/tests/login-modal.spec.ts:12:62

Check failure on line 12 in e2e/tests/login-modal.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright E2E Tests

[login-modal] › e2e/tests/login-modal.spec.ts:9:6 › Login Modal › email/password login via modal

1) [login-modal] › e2e/tests/login-modal.spec.ts:9:6 › Login Modal › email/password login via modal TimeoutError: locator.click: Timeout 15000ms exceeded. Call log: - waiting for getByRole('button', { name: 'Log In' }).first() 10 | await page.goto("/b"); 11 | await page.waitForLoadState("networkidle"); > 12 | await page.getByRole("button", { name: "Log In" }).first().click(); | ^ 13 | await expect(page.getByRole("dialog")).toBeVisible(); 14 | await expect(page.getByText("Login to Continue")).toBeVisible(); 15 | await page.getByLabel("Email").fill(FRAPPE_USER); at /home/runner/work/buzz/buzz/e2e/tests/login-modal.spec.ts:12:62
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByText("Login to Continue")).toBeVisible();
await page.getByLabel("Email").fill(FRAPPE_USER);
Expand All @@ -20,10 +20,10 @@
});

test("shows error for invalid credentials", async ({ page }) => {
await page.goto("/dashboard");
await page.goto("/b");
await page.waitForLoadState("networkidle");

await page.getByRole("button", { name: "Log In" }).first().click();

Check failure on line 26 in e2e/tests/login-modal.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright E2E Tests

[login-modal] › e2e/tests/login-modal.spec.ts:22:6 › Login Modal › shows error for invalid credentials

2) [login-modal] › e2e/tests/login-modal.spec.ts:22:6 › Login Modal › shows error for invalid credentials Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 15000ms exceeded. Call log: - waiting for getByRole('button', { name: 'Log In' }).first() 24 | await page.waitForLoadState("networkidle"); 25 | > 26 | await page.getByRole("button", { name: "Log In" }).first().click(); | ^ 27 | await expect(page.getByRole("dialog")).toBeVisible(); 28 | 29 | await page.getByLabel("Email").fill("wrong@example.com"); at /home/runner/work/buzz/buzz/e2e/tests/login-modal.spec.ts:26:62

Check failure on line 26 in e2e/tests/login-modal.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright E2E Tests

[login-modal] › e2e/tests/login-modal.spec.ts:22:6 › Login Modal › shows error for invalid credentials

2) [login-modal] › e2e/tests/login-modal.spec.ts:22:6 › Login Modal › shows error for invalid credentials Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 15000ms exceeded. Call log: - waiting for getByRole('button', { name: 'Log In' }).first() 24 | await page.waitForLoadState("networkidle"); 25 | > 26 | await page.getByRole("button", { name: "Log In" }).first().click(); | ^ 27 | await expect(page.getByRole("dialog")).toBeVisible(); 28 | 29 | await page.getByLabel("Email").fill("wrong@example.com"); at /home/runner/work/buzz/buzz/e2e/tests/login-modal.spec.ts:26:62

Check failure on line 26 in e2e/tests/login-modal.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright E2E Tests

[login-modal] › e2e/tests/login-modal.spec.ts:22:6 › Login Modal › shows error for invalid credentials

2) [login-modal] › e2e/tests/login-modal.spec.ts:22:6 › Login Modal › shows error for invalid credentials TimeoutError: locator.click: Timeout 15000ms exceeded. Call log: - waiting for getByRole('button', { name: 'Log In' }).first() 24 | await page.waitForLoadState("networkidle"); 25 | > 26 | await page.getByRole("button", { name: "Log In" }).first().click(); | ^ 27 | await expect(page.getByRole("dialog")).toBeVisible(); 28 | 29 | await page.getByLabel("Email").fill("wrong@example.com"); at /home/runner/work/buzz/buzz/e2e/tests/login-modal.spec.ts:26:62
await expect(page.getByRole("dialog")).toBeVisible();

await page.getByLabel("Email").fill("wrong@example.com");
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/offline-payment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ test.describe("Booking Details - Offline Payment", () => {
test("shows verification pending status", async ({ page }) => {
// This test assumes a booking already exists
// Navigate to bookings list
await page.goto("/dashboard/bookings");
await page.goto("/b/bookings");
await page.waitForLoadState("networkidle");

// Look for verification pending badge
Expand Down
Loading