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

View File

@ -135,6 +135,9 @@ var EgwApp = /** @class */ (function () {
/**
* 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 {string} pushData.app application name
* @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
*/
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"
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;
}());
exports.EgwApp = EgwApp;
//# sourceMappingURL=egw_app.js.map

View File

@ -15,6 +15,19 @@ import 'jqueryui';
import '../jsapi/egw_global';
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
* Each app should extend as needed.
@ -206,6 +219,9 @@ export abstract class EgwApp
/**
* 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 {string} pushData.app application name
* @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 {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"
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()
*/
var prefs = {};
var grants = {};
// Return the actual extension
return {
@ -171,6 +172,50 @@ egw.extend('preferences', egw.MODULE_GLOBAL, function()
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;
}
/**
* 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)
*

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)
{
error_log(__METHOD__."($link_id, '$app', $id, ...)");
if (self::DEBUG)
{
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 $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 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);
//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)
Hooks::process([
'location' => 'notify-all',
'type' => 'edit',
'type' => !empty($data[Link::OLD_LINK_TITLE]) ? 'update' : $type,
'app' => $app,
'id' => $id,
'data' => $data,

View File

@ -23,7 +23,7 @@ $replace = array(
"\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([^:\n]+):\s*function\s*\(.*this._super.(apply|call)\(/msU" =>
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
if($old && $old['ts_status'] == self::DELETED_STATUS && $new['ts_status'] != self::DELETED_STATUS)
{
Link::restore(TIMESHEET_APP, $new['ts_id']);
$type = 'add';
}
if (!($err = parent::save()))
@ -659,7 +663,7 @@ class timesheet_bo extends Api\Storage
return implode(', ',$this->tracking->errors);
}
// 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;

View File

@ -38,7 +38,9 @@ var egw_app_1 = require("../../api/js/jsapi/egw_app");
var TimesheetApp = /** @class */ (function (_super) {
__extends(TimesheetApp, _super);
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
@ -170,6 +172,46 @@ var TimesheetApp = /** @class */ (function (_super) {
if (widget)
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;
}(egw_app_1.EgwApp));
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
{
readonly appname: 'timesheet';
readonly appname = 'timesheet';
/**
* 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');
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;