Api: add REST API handler and JSON format for Tracker items#3
Open
amirdhs wants to merge 5 commits into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a REST/JSON layer for the Tracker app analogous to other EGroupware apps' CalDAV handlers. The handler hooks into the GroupDAV endpoint and the JsTracker class converts tracker items to/from a JSON representation.
Changes:
- New
ApiHandler(CalDAVHandlersubclass) implementing PROPFIND/GET/PUT/POST/PATCH/DELETE, sync-collection, filter mapping, ETag and exception handling for/trackercollections. - New
JsTracker(CalDAVJsBasesubclass) providingJsTicket()serialization andparseJsTicket()/parseStatus()/parseAssigned()deserialization including custom-status and custom-field support. - Auto-creates a "Default" tracker queue on first REST use if none exist, so search/list does not return empty.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| src/ApiHandler.php | New REST handler for /tracker collection: routing, filters, sync-collection, ACL, ETag, exception responses. |
| src/JsTracker.php | New JSON renderer/parser for tracker items (JsTicket/parseJsTicket), including status label mapping and assigned/account handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+231
to
+237
| // sync-collection: deleted items have no properties | ||
| if ((string)$ticket['tr_status'] === \tracker_so::STATUS_DELETED) | ||
| { | ||
| yield ['path' => $path . urldecode($this->get_path($entry))]; | ||
| if (++$yielded && isset($nresults) && $yielded >= $nresults) break 2; | ||
| continue; | ||
| } |
| 'cc' => $ticket['cc'] ?: null, | ||
| 'group' => !empty($ticket['group']) ? self::account($ticket['group']) : null, | ||
| 'egroupware.org:customfields' => self::customfields($ticket), | ||
| 'etag' => ApiHandler::etag($ticket), |
Comment on lines
+67
to
+95
| $data = array_filter([ | ||
| self::AT_TYPE => self::TYPE_TICKET, | ||
| 'id' => (int)$ticket['id'], | ||
| 'summary' => $ticket['summary'], | ||
| 'description' => $ticket['description'] ?: null, | ||
| 'tracker' => (int)$ticket['tracker'] ?: null, | ||
| 'status' => $status_label, | ||
| 'priority' => (int)$ticket['priority'], | ||
| 'completion' => (int)$ticket['completion'], | ||
| 'startDate' => !empty($ticket['startdate']) ? self::UTCDateTime($ticket['startdate'], true) : null, | ||
| 'dueDate' => !empty($ticket['duedate']) ? self::UTCDateTime($ticket['duedate'], true) : null, | ||
| 'closed' => !empty($ticket['closed']) ? self::UTCDateTime($ticket['closed'], true) : null, | ||
| 'private' => (bool)$ticket['private'], | ||
| 'category' => self::categories($ticket['cat_id']), | ||
| 'version' => $ticket['version'] ? self::categories($ticket['version']) : null, | ||
| 'creator' => self::account($ticket['creator']), | ||
| 'created' => self::UTCDateTime($ticket['created'], true), | ||
| 'modified' => !empty($ticket['modified']) ? self::UTCDateTime($ticket['modified'], true) : null, | ||
| 'modifier' => !empty($ticket['modifier']) ? self::account($ticket['modifier']) : null, | ||
| 'assigned' => !empty($ticket['assigned']) ? self::assigned($ticket['assigned']) : null, | ||
| 'cc' => $ticket['cc'] ?: null, | ||
| 'group' => !empty($ticket['group']) ? self::account($ticket['group']) : null, | ||
| 'egroupware.org:customfields' => self::customfields($ticket), | ||
| 'etag' => ApiHandler::etag($ticket), | ||
| ]); | ||
|
|
||
| // @type and private must always be present even when falsy | ||
| $data[self::AT_TYPE] = self::TYPE_TICKET; | ||
| $data['private'] = (bool)$ticket['private']; |
Comment on lines
+203
to
+211
| for ( | ||
| $chunk = 0; | ||
| ($tickets = $this->bo->search( | ||
| $criteria, false, $order, '', '', false, 'AND', | ||
| [$initial_offset + $chunk * self::CHUNK_SIZE, $nresults ?: self::CHUNK_SIZE], | ||
| $filter, false | ||
| )); | ||
| ++$chunk | ||
| ) |
Comment on lines
+148
to
+230
| foreach ($data as $name => $value) | ||
| { | ||
| switch ($name) | ||
| { | ||
| case 'summary': | ||
| $ticket['tr_summary'] = $value; | ||
| break; | ||
|
|
||
| case 'description': | ||
| $ticket['tr_description'] = $value; | ||
| break; | ||
|
|
||
| case 'tracker': | ||
| $ticket['tr_tracker'] = self::parseInt($value); | ||
| break; | ||
|
|
||
| case 'status': | ||
| $ticket['tr_status'] = self::parseStatus($value); | ||
| break; | ||
|
|
||
| case 'priority': | ||
| $ticket['tr_priority'] = self::parseInt($value); | ||
| break; | ||
|
|
||
| case 'completion': | ||
| $ticket['tr_completion'] = min(100, max(0, self::parseInt($value))); | ||
| break; | ||
|
|
||
| case 'startDate': | ||
| $ticket['tr_startdate'] = $value ? self::parseDateTime($value) : null; | ||
| break; | ||
|
|
||
| case 'dueDate': | ||
| $ticket['tr_duedate'] = $value ? self::parseDateTime($value) : null; | ||
| break; | ||
|
|
||
| case 'private': | ||
| $ticket['tr_private'] = $value ? 1 : 0; | ||
| break; | ||
|
|
||
| case 'category': | ||
| $ticket['cat_id'] = self::parseCategories($value, false); | ||
| break; | ||
|
|
||
| case 'version': | ||
| $ticket['tr_version'] = $value ? self::parseInt($value) : null; | ||
| break; | ||
|
|
||
| case 'creator': | ||
| $ticket['tr_creator'] = self::parseAccount($value); | ||
| break; | ||
|
|
||
| case 'assigned': | ||
| $ticket['tr_assigned'] = self::parseAssigned($value); | ||
| break; | ||
|
|
||
| case 'cc': | ||
| $ticket['tr_cc'] = $value; | ||
| break; | ||
|
|
||
| case 'group': | ||
| $ticket['tr_group'] = $value ? self::parseAccount($value) : null; | ||
| break; | ||
|
|
||
| case 'egroupware.org:customfields': | ||
| $ticket = array_merge($ticket, self::parseCustomfields($value)); | ||
| break; | ||
|
|
||
| // read-only / auto-set fields — silently ignore | ||
| case self::AT_TYPE: | ||
| case 'id': | ||
| case 'etag': | ||
| case 'created': | ||
| case 'modified': | ||
| case 'modifier': | ||
| case 'closed': | ||
| break; | ||
|
|
||
| default: | ||
| error_log(__METHOD__ . "() unknown field $name=" . json_encode($value, self::JSON_OPTIONS_ERROR) . ' --> ignored'); | ||
| break; | ||
| } | ||
| } |
Comment on lines
+135
to
+148
| $data = json_decode($json, true, 10, JSON_THROW_ON_ERROR); | ||
|
|
||
| // For PATCH: only parse what's in the request body. | ||
| // Do NOT re-serialize $old and merge — that converts raw IDs to display names | ||
| // and causes lookup failures. so_sql::save() will merge the partial update | ||
| // with the existing $this->bo->data that was already loaded by read(). | ||
| if ($method !== 'PATCH' && empty($data['summary'])) | ||
| { | ||
| throw new Api\CalDAV\JsParseException("Required field 'summary' missing"); | ||
| } | ||
|
|
||
| $ticket = []; | ||
|
|
||
| foreach ($data as $name => $value) |
Member
|
Hi Amir, as I said in our AI meeting:
Ralf |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Align propfind paging stride with the fetch size so nresults values above CHUNK_SIZE do not overlap pages, and only emit deleted tickets as path-only entries during sync-collection reports. Based on Copilot review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.