Skip to content

refactor(api): split forms into fields, services and typed responses - #300

Merged
harshtandiya merged 4 commits into
developfrom
refactor/api-forms
Jul 29, 2026
Merged

refactor(api): split forms into fields, services and typed responses#300
harshtandiya merged 4 commits into
developfrom
refactor/api-forms

Conversation

@harshtandiya

@harshtandiyaharshtandiya commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #298 — read that first. Base refactor/api-sponsorships, repoint to develop once that merges.

PR 4 of the buzz/api restructure. forms/__init__.py 416 lines → 36, over fields.py (doctype meta reads), services.py (CustomFormService + the proposal functions), schemas.py, exceptions.py.

form_fields stays list[dict] on purpose — built from the target doctype's meta at runtime, shape varies per field type, so only the envelope is modelled. custom_fields has a fixed get_all field list and is typed.

The four frappe.AuthenticationError throws became one LoginRequired (401). That changes exc_type, which BaseCustomEventForm.vue:297 and EventProposalForm.vue:122 branch on to show the login prompt — both moved in the same commit, checked in the browser. Safe because frappe/app.py:411 clears cookies on e.__class__ == frappe.AuthenticationError, exact class not subclass, and only ever fired for guests.

Both submit endpoints are now methods=["POST"]. They are allow_guest and they insert; frappe/auth.py:85 skips CSRF outside UNSAFE_HTTP_METHODS, so a GET reached them tokenless and then rolled back uncommitted. frappe-ui already POSTs (frappeRequest.ts:83). Reads left unrestricted — they mutate nothing. The rest of buzz/api has the same gap; process_booking and checkin_ticket are worth a look in PRs 5–6.

event.name now travels as a string — Buzz Event autonames to ints and nothing in the dashboard reads the key.

Not changed: no endpoint renamed; buzz_event.py:10 still imports validate_excluded_fields, from fields.py now.

Deleted: the Table re-copy loop in both submit paths — the allowlist loop above it already wrote the same value, and Event Proposal has no Table fields at all.

Deferred, both noted in the plan: create_event_proposal sets submitted_by but Event Proposal has no such field, so the proposer is only ever owner. And get_link_field_options returns every row of a Link target to guests unfiltered — harmless today only because Talk Proposal.submitted_by is excluded by fieldname, so a new Link-to-User under another name would leak emails.

Checks: 48 Python tests (was 23), 31 custom-forms + 14 event-proposal Playwright green headed on :8005, ruff and vue-tsc clean. Both read endpoints byte-identical against a copy of the pre-change module across six seeded forms, the closed branch, and one carrying Buzz Custom Field rows — bar event.name. Other E2E projects touch no file here; the 4 failures there are unrelated (login-modal drives the Buzz modal as Administrator, which cannot work).

🤖 Generated with Claude Code

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@harshtandiya
harshtandiyaforce-pushed the refactor/api-sponsorships branch from 414e4f7 to acea93fCompareJuly 29, 2026 16:45

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@harshtandiya
harshtandiyaforce-pushed the refactor/api-sponsorships branch from acea93f to 272c648CompareJuly 29, 2026 16:53

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

Base automatically changed from refactor/api-sponsorships to developJuly 29, 2026 16:59
harshtandiyaand others added 4 commits July 29, 2026 22:30
forms/__init__.py held 416 lines: five endpoints, nine meta-introspection
helpers, and a get_custom_form_data that built its response dict inline
across three branches. Every refusal was a bare frappe.throw, so a closed
form and a missing event came back as the same generic error.
The module is now four files. fields.py owns everything that reads doctype
meta — form field construction, link options, the exclude-field sets and
their validation. services.py owns CustomFormService, which resolves the
event and its form row as plain properties over get_cached_doc and answers
form_data() or submit(). The event proposal endpoints stay module-level
functions; there is no per-request state there to hang a class on.
__init__.py is 36 lines of endpoints that validate by annotation, delegate
and return.
Responses are pydantic models. form_fields stays list[dict] on purpose:
its shape is built from the target doctype's meta at runtime and varies per
field type, so only the envelope is modelled. custom_fields comes from a
fixed get_all field list and is typed. event.name is sent as a string —
Buzz Event autonames to integers, and nothing in the dashboard reads the
key, so the cast costs nothing and survives a naming change.
Errors are seven named classes carrying their own status and copy: three
404s, a 401, a 409 and two 400s for the excluded-field validation that
runs on Buzz Event save.
The 401 is the one deliberate contract change. BaseCustomEventForm.vue and
EventProposalForm.vue branch on exc_type to swap the form for a login
prompt, so the four frappe.AuthenticationError throws becoming LoginRequired
means both comparisons move in this commit. Nothing else depended on that
class: frappe/app.py clears cookies on an exact class match, not a subclass,
and only ever did so for guests. The two "log in" / "login" copy variants
collapsed into one string.
Two dead things went with it. The Table re-copy loop in submit_custom_form
re-assigned what the allowlist loop above it had already written, and the
same loop in the proposal path targeted fields Event Proposal does not have.
buzz_event.py imports validate_excluded_fields from its new home in
fields.py. No endpoint renamed. Both read endpoints were diffed against a
copy of the pre-change module across six seeded forms, the closed branch and
a form carrying Buzz Custom Field rows, and came out byte-identical apart
from event.name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither login_required nor auto_close_at had any end-to-end coverage, which
is exactly the pair that decides whether a custom form renders at all. The
login gate is also what the dashboard's exc_type branch reads, so a rename
on the server could have silently stopped the login prompt from appearing
and no test would have noticed.
custom-forms.setup.ts seeds two more forms on the shared event: one behind
login_required, one already past its auto_close_at with its own closed copy.
The guest cases run under a storageState override inside the existing
project rather than a new one — they need no session, only the absence of
the shared one.
The closed and not-found panels share bg-surface-amber-1, so expectClosed
and expectNotFound now tell them apart by heading text. expectClosed was
matching bg-surface-orange-1, which renders nowhere; it had no caller, so
nothing failed.
The event proposal spec gains the same guest pair. Its settings flip needs
the shared admin session, since that block runs signed out, so it goes
through newApiContext and restores in a finally.
Seeded identifiers live in e2e/data/custom-forms.ts — Playwright refuses to
let a spec import a setup file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both submit endpoints are allow_guest and both insert a document, but
neither declared its HTTP methods, so both were reachable over GET.
frappe/auth.py:85 skips CSRF validation for anything outside
UNSAFE_HTTP_METHODS, so a GET reached them without a token — and since
frappe only auto-commits on POST/PUT, the insert it triggered was silently
rolled back. Either way it is a path that should not exist.
Both callers already POST: frappe-ui defaults to it
(frappeRequest.ts:83, `options.method || 'POST'`) and the e2e callMethod
helper sets it explicitly. The read endpoints are left unrestricted — they
mutate nothing, so pinning their verb would only risk a caller.
Also moves the two field predicates to the bottom of fields.py and
get_dial_code_list to the bottom of services.py, so each file opens with
the function it exists for rather than its helpers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding methods=["POST"] pushed the two submit decorators past the 110
column limit, so ruff wrapped them and carried the trailing nosemgrep
comment down to the closing paren — off the line semgrep matches. The
guest-whitelisted-method rule is blocking, so the linter job failed.
Moved to a preceding-line comment, which semgrep honours and ruff cannot
reflow. Verified against frappe/semgrep-rules locally: the file scans clean
with the comment and reports the finding without it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@greptile-appsgreptile-appsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@harshtandiya
harshtandiya merged commit 8f7653d into developJul 29, 2026
7 checks passed
@harshtandiya
harshtandiya deleted the refactor/api-forms branch July 29, 2026 17:16
@github-actions

Copy link
Copy Markdown
Contributor

Backport failed for main, because it was unable to cherry-pick the commit(s).

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin main
git worktree add -d .worktree/backport-300-to-main origin/main
cd .worktree/backport-300-to-main
git switch --create backport-300-to-main
git cherry-pick -x 8f7653d194ef94561a666ced552727a84381206d

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport mainbackport to main branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@harshtandiya