[OP-19968] Fix 500 when writing end_time on a time entry - #24775
Conversation
end_time is derived (start_time + hours) and has no setter of its own -- the date_time_property declaration had a custom getter but no setter, so Representable fell through to its default send(:end_time=, value) when a client included endTime in a write payload, crashing with NoMethodError since TimeEntry defines no such method. Add an explicit no-op setter, matching how other purely-computed read-only representer properties in this codebase already handle a write attempt.
|
All contributors have signed the CLA ✍️ ✅ |
|
recheck |
There was a problem hiding this comment.
Pull request overview
Fixes a crash in the Costs module’s API v3 time entry representer when clients include endTime in write payloads, by explicitly handling the field as unwritable (since end_time is derived from start_time + hours and has no model setter).
Changes:
- Add an explicit no-op setter for the computed
end_timerepresenter property to prevent Representable from calling a non-existentend_time=method. - Add a regression parsing spec ensuring payloads containing
endTimeno longer raise (and should be ignored).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| modules/costs/lib/api/v3/time_entries/time_entry_representer.rb | Adds a no-op setter for computed end_time to avoid NoMethodError on write payloads containing endTime. |
| modules/costs/spec/lib/api/v3/time_entries/time_entry_representer_parsing_spec.rb | Adds a regression spec for parsing write payloads that include endTime. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| setter: ->(*) { | ||
| # end_time is derived (start_time + hours), never its own column -- | ||
| # silently accept a write attempt without touching the record, same | ||
| # as any other purely-computed read-only API property. | ||
| }, |
Collapse the setter to a one-line no-op matching this codebase's existing
convention for an ignored representer property (setter: ->(*) {} #
ignored, per query_representer.rb's :hidden property). The regression
spec's respond_to?(:end_time=) assertion was always true/false
independent of whether the payload was actually respected -- TimeEntry
never had that method regardless of the fix. Replaced with an assertion
that a deliberately different endTime value is genuinely ignored,
verified against the value start_time + hours actually derives.
Drop the block comment that only restates the PR description; keep and tighten the two inline notes explaining non-obvious test-design choices (why this specific value, why the manual timezone set).
|
This approach works as a fix for me, but it might be confusing to the user to have it silently dropped. Would outputting an error make more sense instead? /cc @klaustopher |
|
I think the correct approach in the API would be: date_time_property :end_time,
exec_context: :decorator,
getter: ->(*) {
datetime_formatter.format_datetime(represented.end_timestamp, allow_nil: true)
},
writable: false,
if: ->(*) { TimeEntry.can_track_start_and_end_time? }It will have the same effect, but is more speaking to readers than the empty setter. |
|
Yes, better. Matches existing patterns. |
Ticket
https://community.openproject.org/wp/OP-19968
Summary
end_timeis derived (start_time+hours) and has no setter of its own -- thedate_time_propertydeclaration had a custom getter but no setter, so Representable fell through to its defaultsend(:end_time=, value)when a client includedendTimein a write payload, crashing withNoMethodErrorsinceTimeEntrydefines no such method.Change
Add an explicit no-op setter, matching how other purely-computed read-only representer properties in this codebase already handle a write attempt.
Test plan
endTimeis included in a write payloadPOSTwithendTimeset → 500; after the fix → 201, entry created successfully