complete push implementation for timesheet incl. ACL check

This commit is contained in:
Ralf Becker 2020-01-24 13:31:56 +01:00
parent 76a5793a0a
commit e9c4d3f07e
10 changed files with 271 additions and 10 deletions

View File

@ -417,6 +417,7 @@
egw_script.getAttribute('data-websocket-url'), egw_script.getAttribute('data-websocket-url'),
JSON.parse(egw_script.getAttribute('data-websocket-tokens')) JSON.parse(egw_script.getAttribute('data-websocket-tokens'))
); );
egw.set_grants(JSON.parse(egw_script.getAttribute('data-grants') || "{}"));
} }
} }
catch(e) { catch(e) {
@ -440,7 +441,7 @@
// get TypeScript modules working with our loader // get TypeScript modules working with our loader
function require(_file) function require(_file)
{ {
return { EgwApp: window.EgwApp}; return window.exports;
} }
var exports = {}; var exports = {};

View File

@ -135,6 +135,9 @@ var EgwApp = /** @class */ (function () {
/** /**
* Handle a push notification about entry changes from the websocket * Handle a push notification about entry changes from the websocket
* *
* Get's called for data of all apps, but should only handle data of apps it displays,
* which is by default only it's own, but can be for multiple apps eg. for calendar.
*
* @param pushData * @param pushData
* @param {string} pushData.app application name * @param {string} pushData.app application name
* @param {(string|number)} pushData.id id of entry to refresh or null * @param {(string|number)} pushData.id id of entry to refresh or null
@ -148,9 +151,40 @@ var EgwApp = /** @class */ (function () {
* @param {number} pushData.account_id User that caused the notification * @param {number} pushData.account_id User that caused the notification
*/ */
EgwApp.prototype.push = function (pushData) { EgwApp.prototype.push = function (pushData) {
// don't care about other apps data, reimplement if your app does care eg. calendar
if (pushData.app !== this.appname)
return;
// only handle delete by default, for simple case of uid === "$app::$id" // only handle delete by default, for simple case of uid === "$app::$id"
if (pushData.type === 'delete') { if (pushData.type === 'delete') {
egw.dataStoreUID(pushData.app + '::' + pushData.id, null); egw.dataStoreUID(this.uid(pushData), null);
}
};
/**
* Get (possible) app-specific uid
*
* @param {object} pushData see push method for individual attributes
*/
EgwApp.prototype.uid = function (pushData) {
return pushData.app + '::' + pushData.id;
};
/**
* Method called after apps push implementation checked visibility
*
* @param {et2_nextmatch} nm
* @param pushData see push method for individual attributes
* @todo implement better way to update nextmatch widget without disturbing the user / state
* @todo show indicator that an update has happend
* @todo rate-limit update frequency
*/
EgwApp.prototype.updateList = function (nm, pushData) {
switch (pushData.type) {
case 'add':
case 'unknown':
nm.applyFilters();
break;
default:
egw.dataRefreshUID(this.uid(pushData));
break;
} }
}; };
/** /**
@ -1634,3 +1668,4 @@ var EgwApp = /** @class */ (function () {
return EgwApp; return EgwApp;
}()); }());
exports.EgwApp = EgwApp; exports.EgwApp = EgwApp;
//# sourceMappingURL=egw_app.js.map

View File

@ -15,6 +15,19 @@ import 'jqueryui';
import '../jsapi/egw_global'; import '../jsapi/egw_global';
import '../etemplate/et2_types'; import '../etemplate/et2_types';
/**
* Type for push-message
*/
export interface PushData
{
type: "add"|"edit"|"update"|"delete"|"unknown";
app: string; // app-name, can include a subtype eg. "projectmanager-element"
id: string | number;
acl?: any; // app-specific acl data, eg. the owner, or array of participants
account_id: number; // user that caused the change
[propName:string]: any; // arbitrary more parameters
}
/** /**
* Common base class for application javascript * Common base class for application javascript
* Each app should extend as needed. * Each app should extend as needed.
@ -206,6 +219,9 @@ export abstract class EgwApp
/** /**
* Handle a push notification about entry changes from the websocket * Handle a push notification about entry changes from the websocket
* *
* Get's called for data of all apps, but should only handle data of apps it displays,
* which is by default only it's own, but can be for multiple apps eg. for calendar.
*
* @param pushData * @param pushData
* @param {string} pushData.app application name * @param {string} pushData.app application name
* @param {(string|number)} pushData.id id of entry to refresh or null * @param {(string|number)} pushData.id id of entry to refresh or null
@ -218,12 +234,49 @@ export abstract class EgwApp
* @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary * @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary
* @param {number} pushData.account_id User that caused the notification * @param {number} pushData.account_id User that caused the notification
*/ */
push(pushData) push(pushData : PushData)
{ {
// don't care about other apps data, reimplement if your app does care eg. calendar
if (pushData.app !== this.appname) return;
// only handle delete by default, for simple case of uid === "$app::$id" // only handle delete by default, for simple case of uid === "$app::$id"
if (pushData.type === 'delete') if (pushData.type === 'delete')
{ {
egw.dataStoreUID(pushData.app + '::' + pushData.id, null); egw.dataStoreUID(this.uid(pushData), null);
}
}
/**
* Get (possible) app-specific uid
*
* @param {object} pushData see push method for individual attributes
*/
uid(pushData)
{
return pushData.app + '::' + pushData.id;
}
/**
* Method called after apps push implementation checked visibility
*
* @param {et2_nextmatch} nm
* @param pushData see push method for individual attributes
* @todo implement better way to update nextmatch widget without disturbing the user / state
* @todo show indicator that an update has happend
* @todo rate-limit update frequency
*/
updateList(nm, pushData : PushData)
{
switch (pushData.type)
{
case 'add':
case 'unknown':
nm.applyFilters();
break;
default:
egw.dataRefreshUID(this.uid(pushData));
break;
} }
} }

View File

@ -25,6 +25,7 @@ egw.extend('preferences', egw.MODULE_GLOBAL, function()
* @access: private, use egw.preferences() or egw.set_perferences() * @access: private, use egw.preferences() or egw.set_perferences()
*/ */
var prefs = {}; var prefs = {};
var grants = {};
// Return the actual extension // Return the actual extension
return { return {
@ -171,6 +172,50 @@ egw.extend('preferences', egw.MODULE_GLOBAL, function()
break; break;
} }
} }
},
/**
* Setting prefs for an app or 'common'
*
* @param {object} _data
* @param {string} _app application name or undefined to set grants of all apps at once
* and therefore will be inaccessible in IE, after that window is closed
*/
set_grants: function(_data, _app)
{
if (_app)
{
grants[_app] = jQuery.extend(true, {}, _data);
}
else
{
grants = jQuery.extend(true, {}, _data);
}
},
/**
* Query an EGroupware user preference
*
* We currently load grants from all apps in egw.js, so no need for a callback or promise.
*
* @param {string} _app app-name
* @param {function|false|undefined} _callback optional callback, if preference needs loading first
* if false given and preference is not loaded, undefined is return and no (synchronious) request is send to server
* @param {object} _context context for callback
* @return {object|undefined|false} grant object, false if not (yet) loaded and no callback or undefined
*/
grants: function( _app) //, _callback, _context)
{
/* we currently load grants from all apps in egw.js, so no need for a callback or promise
if (typeof grants[_app] == 'undefined')
{
if (_callback === false) return undefined;
var request = this.json('EGroupware\\Api\\Framework::ajax_get_preference', [_app], _callback, _context);
request.sendRequest(typeof _callback == 'function', 'GET'); // use synchronous (cachable) GET request
if (typeof grants[_app] == 'undefined') grants[_app] = {};
if (typeof _callback == 'function') return false;
}*/
return typeof grants[_app] === 'object' ? jQuery.extend({}, grants[_app]) : grants[_app];
} }
}; };
}); });

View File

@ -718,6 +718,38 @@ class Acl
return $grants; return $grants;
} }
/**
* Get grants for a single app
*
* We use a "get-grants" hook, in case an app need more then what Acl::get_grants returns (with default parameters!).
*
* @param string $_app app-name
* @return array
*/
function ajax_get_grants($_app=null)
{
if (!($grants = Hooks::single('get-grants', $_app)))
{
$grants = $this->get_grants($_app);
}
return $grants;
}
/**
* Get grants for all apps
*
* @return array with app => array of grants pairs
*/
function ajax_get_all_grants()
{
$app_grants = [];
foreach(array_keys($GLOBALS['egw_info']['user']['apps']) as $app)
{
$app_grants[$app] = $this->get_grants($app);
}
return $app_grants;
}
/** /**
* Deletes all ACL entries for an account (user or group) * Deletes all ACL entries for an account (user or group)
* *

View File

@ -677,6 +677,7 @@ class Link extends Link\Storage
*/ */
static function unlink2($link_id,$app,&$id,$owner=0,$app2='',$id2='',$hold_for_purge=false) static function unlink2($link_id,$app,&$id,$owner=0,$app2='',$id2='',$hold_for_purge=false)
{ {
error_log(__METHOD__."($link_id, '$app', $id, ...)");
if (self::DEBUG) if (self::DEBUG)
{ {
echo "<p>Link::unlink('$link_id','$app',".array2string($id).",'$owner','$app2','$id2', $hold_for_purge)</p>\n"; echo "<p>Link::unlink('$link_id','$app',".array2string($id).",'$owner','$app2','$id2', $hold_for_purge)</p>\n";
@ -1479,8 +1480,9 @@ class Link extends Link\Storage
* @param string $app name of app in which the updated happend * @param string $app name of app in which the updated happend
* @param string $id id in $app of the updated entry * @param string $id id in $app of the updated entry
* @param array $data =null updated data of changed entry, as the read-method of the BO-layer would supply it * @param array $data =null updated data of changed entry, as the read-method of the BO-layer would supply it
* @param string $type ="unknown" type of update: "add", "edit", "update" or default "unknown"
*/ */
static function notify_update($app,$id,$data=null) static function notify_update($app,$id,$data=null,$type='unknown')
{ {
self::delete_cache($app,$id); self::delete_cache($app,$id);
//error_log(__METHOD__."('$app', $id, $data)"); //error_log(__METHOD__."('$app', $id, $data)");
@ -1497,7 +1499,7 @@ class Link extends Link\Storage
// in case "someone" interested in all changes (used eg. for push) // in case "someone" interested in all changes (used eg. for push)
Hooks::process([ Hooks::process([
'location' => 'notify-all', 'location' => 'notify-all',
'type' => 'edit', 'type' => !empty($data[Link::OLD_LINK_TITLE]) ? 'update' : $type,
'app' => $app, 'app' => $app,
'id' => $id, 'id' => $id,
'data' => $data, 'data' => $data,

View File

@ -23,7 +23,7 @@ $replace = array(
"\n});" => "\n}\n\napp.classes.$matches[1] = ".ucfirst($matches[1])."App;" "\n});" => "\n}\n\napp.classes.$matches[1] = ".ucfirst($matches[1])."App;"
]); ]);
}, },
"/^\tappname:\s*'([^']+)',/m" => "\treadonly appname: '$1';", "/^\tappname:\s*'([^']+)',/m" => "\treadonly appname = '$1';",
"/^\t([^: ,;(]+):\s*([^()]+),/m" => "\t\$1: $2;", "/^\t([^: ,;(]+):\s*([^()]+),/m" => "\t\$1: $2;",
"/^\t([^:\n]+):\s*function\s*\(.*this._super.(apply|call)\(/msU" => "/^\t([^:\n]+):\s*function\s*\(.*this._super.(apply|call)\(/msU" =>
function($matches) { function($matches) {

View File

@ -640,10 +640,14 @@ class timesheet_bo extends Api\Storage
} }
} }
$type = !isset($old) ? 'add' :
($new['ts_status'] == self::DELETED_STATUS ? 'delete' : 'update');
// Check for restore of deleted contact, restore held links // Check for restore of deleted contact, restore held links
if($old && $old['ts_status'] == self::DELETED_STATUS && $new['ts_status'] != self::DELETED_STATUS) if($old && $old['ts_status'] == self::DELETED_STATUS && $new['ts_status'] != self::DELETED_STATUS)
{ {
Link::restore(TIMESHEET_APP, $new['ts_id']); Link::restore(TIMESHEET_APP, $new['ts_id']);
$type = 'add';
} }
if (!($err = parent::save())) if (!($err = parent::save()))
@ -659,7 +663,7 @@ class timesheet_bo extends Api\Storage
return implode(', ',$this->tracking->errors); return implode(', ',$this->tracking->errors);
} }
// notify the link-class about the update, as other apps may be subscribt to it // notify the link-class about the update, as other apps may be subscribt to it
Link::notify_update(TIMESHEET_APP,$this->data['ts_id'],$this->data); Link::notify_update(TIMESHEET_APP, $this->data['ts_id'], $this->data, $type);
} }
return $err; return $err;

View File

@ -38,7 +38,9 @@ var egw_app_1 = require("../../api/js/jsapi/egw_app");
var TimesheetApp = /** @class */ (function (_super) { var TimesheetApp = /** @class */ (function (_super) {
__extends(TimesheetApp, _super); __extends(TimesheetApp, _super);
function TimesheetApp() { function TimesheetApp() {
return _super !== null && _super.apply(this, arguments) || this; var _this = _super !== null && _super.apply(this, arguments) || this;
_this.appname = 'timesheet';
return _this;
} }
/** /**
* This function is called when the etemplate2 object is loaded * This function is called when the etemplate2 object is loaded
@ -170,6 +172,46 @@ var TimesheetApp = /** @class */ (function (_super) {
if (widget) if (widget)
return widget.options.value; return widget.options.value;
}; };
/**
* Handle a push notification about entry changes from the websocket
*
* @param pushData
* @param {string} pushData.app application name
* @param {(string|number)} pushData.id id of entry to refresh or null
* @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null
* - update: request just modified data from given rows. Sorting is not considered,
* so if the sort field is changed, the row will not be moved.
* - edit: rows changed, but sorting may be affected. Requires full reload.
* - delete: just delete the given rows clientside (no server interaction neccessary)
* - add: requires full reload for proper sorting
* @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary
* @param {number} pushData.account_id User that caused the notification
*/
TimesheetApp.prototype.push = function (pushData) {
var _a, _b, _c;
// timesheed does NOT care about other apps data
if (pushData.app !== this.appname)
return;
if (pushData.type === 'delete') {
return _super.prototype.push.call(this, pushData);
}
// all other cases (add, edit, update) are handled identical
// check visibility
if (typeof this._grants === 'undefined') {
this._grants = egw.grants(this.appname);
}
if (typeof this._grants[pushData.acl] === 'undefined')
return;
// check if we might not see it because of an owner filter
var nm = (_a = this.et2) === null || _a === void 0 ? void 0 : _a.getWidgetById('nm');
var nm_value = (_b = nm) === null || _b === void 0 ? void 0 : _b.getValue();
if (nm && nm_value && typeof ((_c = nm_value.col_filter) === null || _c === void 0 ? void 0 : _c.ts_owner) !== 'undefined') {
if (!nm_value.col_filter.ts_owner || nm_value.col_filter.ts_owner == pushData.acl) {
this.updateList(nm, pushData);
}
}
};
return TimesheetApp; return TimesheetApp;
}(egw_app_1.EgwApp)); }(egw_app_1.EgwApp));
app.classes.timesheet = TimesheetApp; app.classes.timesheet = TimesheetApp;
//# sourceMappingURL=app.js.map

View File

@ -26,7 +26,7 @@ import { EgwApp } from '../../api/js/jsapi/egw_app';
*/ */
class TimesheetApp extends EgwApp class TimesheetApp extends EgwApp
{ {
readonly appname: 'timesheet'; readonly appname = 'timesheet';
/** /**
* This function is called when the etemplate2 object is loaded * This function is called when the etemplate2 object is loaded
@ -191,6 +191,53 @@ class TimesheetApp extends EgwApp
var widget = this.et2.getWidgetById('ts_title'); var widget = this.et2.getWidgetById('ts_title');
if(widget) return widget.options.value; if(widget) return widget.options.value;
} }
private _grants : any;
/**
* Handle a push notification about entry changes from the websocket
*
* @param pushData
* @param {string} pushData.app application name
* @param {(string|number)} pushData.id id of entry to refresh or null
* @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null
* - update: request just modified data from given rows. Sorting is not considered,
* so if the sort field is changed, the row will not be moved.
* - edit: rows changed, but sorting may be affected. Requires full reload.
* - delete: just delete the given rows clientside (no server interaction neccessary)
* - add: requires full reload for proper sorting
* @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary
* @param {number} pushData.account_id User that caused the notification
*/
push(pushData)
{
// timesheed does NOT care about other apps data
if (pushData.app !== this.appname) return;
if (pushData.type === 'delete')
{
return super.push(pushData);
}
// all other cases (add, edit, update) are handled identical
// check visibility
if (typeof this._grants === 'undefined')
{
this._grants = egw.grants(this.appname);
}
if (typeof this._grants[pushData.acl] === 'undefined') return;
// check if we might not see it because of an owner filter
let nm = this.et2?.getWidgetById('nm');
let nm_value = nm?.getValue();
if (nm && nm_value && typeof nm_value.col_filter?.ts_owner !== 'undefined')
{
if (!nm_value.col_filter.ts_owner || nm_value.col_filter.ts_owner == pushData.acl)
{
this.updateList(nm, pushData);
}
}
}
} }
app.classes.timesheet = TimesheetApp; app.classes.timesheet = TimesheetApp;