* Timesheet: new REST API to query, update and delete timesheets https://github.com/EGroupware/egroupware/blob/master/doc/REST-CalDAV-CardDAV/Timesheet.md
This commit is contained in:
parent
59619f83a9
commit
087e969f9f
|
@ -652,7 +652,7 @@ class addressbook_groupdav extends Api\CalDAV\Handler
|
|||
return $contact;
|
||||
}
|
||||
// jsContact or vCard
|
||||
if (($type=Api\CalDAV::isJSON()))
|
||||
if (($type=Api\CalDAV::isJSON($_SERVER['HTTP_ACCEPT'])) || ($type=Api\CalDAV::isJSON()))
|
||||
{
|
||||
$options['data'] = $contact['list_id'] ? JsContact::getJsCardGroup($contact, $type) :
|
||||
JsContact::getJsCard($contact, $type);
|
||||
|
|
|
@ -1043,7 +1043,7 @@ class CalDAV extends HTTP_WebDAV_Server
|
|||
*/
|
||||
function PATCH(array &$options)
|
||||
{
|
||||
if (!preg_match('#^application/([^; +]+\+)?json#', $_SERVER['HTTP_CONTENT_TYPE']))
|
||||
if (!self::isJSON())
|
||||
{
|
||||
return '501 Not implemented';
|
||||
}
|
||||
|
@ -1529,8 +1529,8 @@ class CalDAV extends HTTP_WebDAV_Server
|
|||
// for some reason OS X Addressbook (CFNetwork user-agent) uses now (DAV:add-member given with collection URL+"?add-member")
|
||||
// POST to the collection URL plus a UID like name component (like for regular PUT) to create new entrys
|
||||
if (isset($_GET['add-member']) || Handler::get_agent() == 'cfnetwork' ||
|
||||
// addressbook has not implemented a POST handler, therefore we have to call the PUT handler
|
||||
preg_match('#^(/[^/]+)?/(addressbook|calendar)(-[^/]+)?/$#', $options['path']) && self::isJSON())
|
||||
// REST API: all but mail have no POST handler, therefore we have to call the PUT handler
|
||||
!preg_match('#^(/[^/]+)?/mail/$#', $options['path']) && self::isJSON())
|
||||
{
|
||||
$_GET['add-member'] = ''; // otherwise we give no Location header
|
||||
return $this->PUT($options, 'POST');
|
||||
|
@ -2049,7 +2049,7 @@ class CalDAV extends HTTP_WebDAV_Server
|
|||
return '404 Not Found';
|
||||
}
|
||||
// REST API & PATCH only implemented for addressbook and calendar currently
|
||||
if (!in_array($app, ['addressbook', 'calendar']) && $method === 'PATCH')
|
||||
if (!self::isJSON() && $method === 'PATCH')
|
||||
{
|
||||
return '501 Not implemented';
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ abstract class Handler
|
|||
'GET' => Api\Acl::READ,
|
||||
'PUT' => Api\Acl::EDIT,
|
||||
'PATCH' => Api\Acl::EDIT,
|
||||
'POST' => Api\Acl::ADD,
|
||||
'DELETE' => Api\Acl::DELETE,
|
||||
);
|
||||
/**
|
||||
|
|
|
@ -755,7 +755,7 @@ class calendar_groupdav extends Api\CalDAV\Handler
|
|||
}
|
||||
|
||||
// jsEvent or iCal
|
||||
if (($type=Api\CalDAV::isJSON()))
|
||||
if (($type=Api\CalDAV::isJSON($_SERVER['HTTP_ACCEPT'])) || ($type=Api\CalDAV::isJSON()))
|
||||
{
|
||||
$options['data'] = $this->iCal($event, $user, strpos($options['path'], '/inbox/') !== false ? 'REQUEST' : null, false, null, $type);
|
||||
$options['mimetype'] = Api\CalDAV\JsCalendar::MIME_TYPE_JSEVENT.';charset=utf-8';
|
||||
|
|
|
@ -44,10 +44,10 @@ curl https://example.org/egroupware/groupdav.php/<username>/timesheet/ -H "Accep
|
|||
"quantity": 2.5,
|
||||
"unitprice": 50,
|
||||
"category": { "other": true },
|
||||
"owner": "ralf@boulder.egroupware.org",
|
||||
"owner": "ralf@example.org",
|
||||
"created": "2005-12-16T23:00:00Z",
|
||||
"modified": "2011-06-08T10:51:20Z",
|
||||
"modifier": "ralf@boulder.egroupware.org",
|
||||
"modifier": "ralf@example.org",
|
||||
"status": "genehmigt",
|
||||
"etag": "1:1307537480"
|
||||
},
|
||||
|
@ -58,10 +58,10 @@ curl https://example.org/egroupware/groupdav.php/<username>/timesheet/ -H "Accep
|
|||
"start": "2016-08-22T12:12:00Z",
|
||||
"duration": 60,
|
||||
"quantity": 1,
|
||||
"owner": "ralf@boulder.egroupware.org",
|
||||
"owner": "ralf@example.org",
|
||||
"created": "2016-08-22T12:12:00Z",
|
||||
"modified": "2016-08-22T13:13:22Z",
|
||||
"modifier": "ralf@boulder.egroupware.org",
|
||||
"modifier": "ralf@example.org",
|
||||
"egroupware.org:customfields": {
|
||||
"auswahl": {
|
||||
"value": [
|
||||
|
@ -187,10 +187,13 @@ curl 'https://example.org/egroupware/groupdav.php/timesheet/140' -H "Accept: app
|
|||
"start": "2016-08-22T12:12:00Z",
|
||||
"duration": 60,
|
||||
"quantity": 1,
|
||||
"owner": "ralf@boulder.egroupware.org",
|
||||
"project": "2024-0001: Test Project",
|
||||
"unitprice": 100.0,
|
||||
"pricelist": 123,
|
||||
"owner": "ralf@example.org",
|
||||
"created": "2016-08-22T12:12:00Z",
|
||||
"modified": "2016-08-22T13:13:22Z",
|
||||
"modifier": "ralf@boulder.egroupware.org",
|
||||
"modifier": "ralf@example.org",
|
||||
"egroupware.org:customfields": {
|
||||
"auswahl": {
|
||||
"value": [
|
||||
|
@ -216,14 +219,33 @@ curl 'https://example.org/egroupware/groupdav.php/timesheet/140' -H "Accept: app
|
|||
<summary>Example: POST request to create a new resource</summary>
|
||||
|
||||
```
|
||||
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/timesheet/' -X POST -d @- -H "Content-Type: application/json" --user <username>
|
||||
cat <<EOF | curl -i -X POST 'https://example.org/egroupware/groupdav.php/<username>/timesheet/' -d @- -H "Content-Type: application/json" -H 'Accept: application/pretty+json' -H 'Prefer: return=representation' --user <username>
|
||||
{
|
||||
TODO
|
||||
"@type": "timesheet",
|
||||
"title": "5. Test Ralf",
|
||||
"start": "2024-02-06T10:00:00Z",
|
||||
"duration": 60
|
||||
}
|
||||
EOF
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Location: https://example.org/egroupware/groupdav.php/<username>/timesheet/1234
|
||||
Content-Type: application/json
|
||||
Location: /egroupware/groupdav.php/ralf/timesheet/204
|
||||
ETag: "204:1707233040"
|
||||
|
||||
{
|
||||
"@type": "timesheet",
|
||||
"id": 204,
|
||||
"title": "5. Test Ralf",
|
||||
"start": "2024-02-06T10:00:00Z",
|
||||
"duration": 60,
|
||||
"quantity": 1,
|
||||
"owner": "ralf@example.org",
|
||||
"created": "2024-02-06T14:24:05Z",
|
||||
"modified": "2024-02-06T14:24:00Z",
|
||||
"modifier": "ralf@example.org",
|
||||
"etag": "204:1707233040"
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
@ -233,9 +255,17 @@ Location: https://example.org/egroupware/groupdav.php/<username>/timesheet/1234
|
|||
<summary>Example: PUT request to update a resource</summary>
|
||||
|
||||
```
|
||||
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -X PUT -d @- -H "Content-Type: application/json" --user <username>
|
||||
cat <<EOF | curl -i -X PUT 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -d @- -H "Content-Type: application/json" --user <username>
|
||||
{
|
||||
TODO
|
||||
"@type": "timesheet",
|
||||
"title": "6. Test Ralf",
|
||||
"start": "2024-02-06T10:00:00Z",
|
||||
"duration": 60,
|
||||
"quantity": 1,
|
||||
"owner": "ralf@example.org",
|
||||
"created": "2024-02-06T14:24:05Z",
|
||||
"modified": "2024-02-06T14:24:00Z",
|
||||
"modifier": "ralf@example.org",
|
||||
}
|
||||
EOF
|
||||
|
||||
|
@ -248,12 +278,12 @@ HTTP/1.1 204 No Content
|
|||
* **PATCH** request with a ```Content-Type: application/json``` header allow to modify a single resource by only specifying changed attributes as a [PatchObject](https://www.rfc-editor.org/rfc/rfc8984.html#type-PatchObject)
|
||||
|
||||
<details>
|
||||
<summary>Example: PATCH request to modify a contact with partial data</summary>
|
||||
<summary>Example: PATCH request to modify a timesheet with partial data</summary>
|
||||
|
||||
```
|
||||
cat <<EOF | curl -i 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -X PATCH -d @- -H "Content-Type: application/json" --user <username>
|
||||
cat <<EOF | curl -i -X PATCH 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -d @- -H "Content-Type: application/json" --user <username>
|
||||
{
|
||||
TODO
|
||||
"status": "invoiced"
|
||||
}
|
||||
EOF
|
||||
|
||||
|
@ -263,4 +293,14 @@ HTTP/1.1 204 No content
|
|||
|
||||
* **DELETE** requests delete single resources
|
||||
|
||||
<details>
|
||||
<summary>Example: DELETE request to delete a timesheet</summary>
|
||||
|
||||
```
|
||||
curl -i -X DELETE 'https://example.org/egroupware/groupdav.php/<username>/timesheet/1234' -H "Accept: application/json" --user <username>
|
||||
|
||||
HTTP/1.1 204 No content
|
||||
```
|
||||
</details>
|
||||
|
||||
> one can use ```Accept: application/pretty+json``` to receive pretty-printed JSON eg. for debugging and exploring the API
|
|
@ -377,6 +377,10 @@ class timesheet_bo extends Api\Storage
|
|||
{
|
||||
$data =& $this->data;
|
||||
}
|
||||
if (!$data)
|
||||
{
|
||||
return null; // entry not found
|
||||
}
|
||||
if (!is_array($data))
|
||||
{
|
||||
$save_data = $this->data;
|
||||
|
|
|
@ -542,8 +542,8 @@ class ApiHandler extends Api\CalDAV\Handler
|
|||
|
||||
try
|
||||
{
|
||||
// jsContact or vCard
|
||||
if (($type=Api\CalDAV::isJSON()))
|
||||
// only JsTimesheet, no *DAV
|
||||
if (($type=Api\CalDAV::isJSON($_SERVER['HTTP_ACCEPT'])) || ($type=Api\CalDAV::isJSON()))
|
||||
{
|
||||
$options['data'] = JsTimesheet::JsTimesheet($timesheet, $type);
|
||||
$options['mimetype'] = 'application/json';
|
||||
|
@ -646,19 +646,20 @@ class ApiHandler extends Api\CalDAV\Handler
|
|||
}
|
||||
if ($this->http_if_match) $timesheet['etag'] = self::etag2value($this->http_if_match);
|
||||
|
||||
if (!($save_ok = $this->bo->save($timesheet)))
|
||||
if (($err = $this->bo->save($timesheet)))
|
||||
{
|
||||
if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($timesheet).") failed, Ok=$save_ok");
|
||||
if ($save_ok === 0)
|
||||
if ($this->debug) error_log(__METHOD__."(,$id) save(".array2string($timesheet).") failed, error=$err");
|
||||
if ($err !== true)
|
||||
{
|
||||
// honor Prefer: return=representation for 412 too (no need for client to explicitly reload)
|
||||
$this->check_return_representation($options, $id, $user);
|
||||
return '412 Precondition Failed';
|
||||
}
|
||||
return '403 Forbidden'; // happens when writing new entries in AB's without ADD rights
|
||||
return '403 Forbidden';
|
||||
}
|
||||
$timesheet = Api\Db::strip_array_keys($this->bo->data, 'ts_');
|
||||
|
||||
// send evtl. necessary response headers: Location, etag, ...
|
||||
// send necessary response headers: Location, etag, ...
|
||||
$this->put_response_headers($timesheet, $options['path'], $retval);
|
||||
|
||||
if ($this->debug > 1) error_log(__METHOD__."(,'$id', $user, '$prefix') returning ".array2string($retval));
|
||||
|
|
|
@ -92,16 +92,16 @@ class JsTimesheet extends Api\CalDAV\JsBase
|
|||
if ($method === 'PATCH')
|
||||
{
|
||||
// apply patch on JsCard of contact
|
||||
$data = self::patch($data, $old ? self::getJsCalendar($old, false) : [], !$old);
|
||||
$data = self::patch($data, $old ? self::JsTimesheet($old, false) : [], !$old);
|
||||
}
|
||||
|
||||
if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist
|
||||
//if (!isset($data['uid'])) $data['uid'] = null; // to fail below, if it does not exist
|
||||
|
||||
// check required fields
|
||||
if (!$old || !$method === 'PATCH')
|
||||
{
|
||||
static $required = ['title', 'start', 'duration'];
|
||||
if (($missing = array_diff_key(array_filter(array_intersect_key($data), array_flip($required)), array_flip($required))))
|
||||
if (($missing = array_diff_key(array_filter(array_intersect_key($data, array_flip($required))), array_flip($required))))
|
||||
{
|
||||
throw new Api\CalDAV\JsParseException("Required field(s) ".implode(', ', $missing)." missing");
|
||||
}
|
||||
|
@ -124,6 +124,15 @@ class JsTimesheet extends Api\CalDAV\JsBase
|
|||
|
||||
case 'duration':
|
||||
$timesheet['ts_duration'] = self::parseInt($value);
|
||||
// set default quantity, if none explicitly given
|
||||
if (!isset($timesheet['ts_quantity']))
|
||||
{
|
||||
$timesheet['ts_quantity'] = $timesheet['ts_duration'] / 60.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pricelist':
|
||||
$timesheet['pl_id'] = self::parseInt($value);
|
||||
break;
|
||||
|
||||
case 'quantity':
|
||||
|
@ -151,6 +160,8 @@ class JsTimesheet extends Api\CalDAV\JsBase
|
|||
case 'created':
|
||||
case 'modified':
|
||||
case 'modifier':
|
||||
case self::AT_TYPE:
|
||||
case 'id':
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
Loading…
Reference in New Issue