mirror of
https://github.com/EGroupware/egroupware.git
synced 2024-11-07 08:34:29 +01:00
Apply client-side push refactoring to calendar, infolog, timesheet
This commit is contained in:
parent
dd97c0f316
commit
df54dcace4
@ -199,9 +199,12 @@ var EgwApp = /** @class */ (function () {
|
||||
*
|
||||
* @param pushData
|
||||
* @param grant_fields List of fields in pushData.acl with account IDs that might grant access eg: info_responsible
|
||||
* @param appname Optional, to check against the grants for a different application. Defaults to this.appname.
|
||||
*
|
||||
* @return boolean Entry has ACL access
|
||||
*/
|
||||
EgwApp.prototype._push_grant_check = function (pushData, grant_fields) {
|
||||
var grants = egw.grants(this.appname);
|
||||
EgwApp.prototype._push_grant_check = function (pushData, grant_fields, appname) {
|
||||
var grants = egw.grants(appname || this.appname);
|
||||
// No grants known
|
||||
if (!grants)
|
||||
return true;
|
||||
|
@ -324,10 +324,13 @@ export abstract class EgwApp
|
||||
*
|
||||
* @param pushData
|
||||
* @param grant_fields List of fields in pushData.acl with account IDs that might grant access eg: info_responsible
|
||||
* @param appname Optional, to check against the grants for a different application. Defaults to this.appname.
|
||||
*
|
||||
* @return boolean Entry has ACL access
|
||||
*/
|
||||
_push_grant_check(pushData : PushData, grant_fields : string[]) : boolean
|
||||
_push_grant_check(pushData : PushData, grant_fields : string[], appname? : string) : boolean
|
||||
{
|
||||
let grants = egw.grants(this.appname);
|
||||
let grants = egw.grants(appname || this.appname);
|
||||
|
||||
// No grants known
|
||||
if(!grants) return true;
|
||||
|
@ -534,17 +534,21 @@ var CalendarApp = /** @class */ (function (_super) {
|
||||
* @param {number} pushData.account_id User that caused the notification
|
||||
*/
|
||||
CalendarApp.prototype.push = function (pushData) {
|
||||
// Calendar cares about calendar & infolog
|
||||
if (pushData.app !== this.appname && pushData.app !== 'infolog')
|
||||
return;
|
||||
switch (pushData.app) {
|
||||
case "calendar":
|
||||
if (pushData.type === 'delete') {
|
||||
return _super.prototype.push.call(this, pushData);
|
||||
}
|
||||
return this.push_calendar(pushData);
|
||||
case "infolog":
|
||||
return this.push_infolog(pushData);
|
||||
default:
|
||||
if (jQuery.extend([], egw.preference("integration_toggle", "calendar")).indexOf(pushData.app) >= 0) {
|
||||
if (pushData.app == "infolog") {
|
||||
return this.push_infolog(pushData);
|
||||
}
|
||||
// Other integration here
|
||||
// TODO
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
};
|
||||
/**
|
||||
@ -555,17 +559,19 @@ var CalendarApp = /** @class */ (function (_super) {
|
||||
CalendarApp.prototype.push_infolog = function (pushData) {
|
||||
var _this = this;
|
||||
var _a;
|
||||
// Check if we have access
|
||||
if (!this._push_grant_check(pushData, ["info_owner", "info_responsible"], "infolog")) {
|
||||
return;
|
||||
}
|
||||
// check visibility - grants is ID => permission of people we're allowed to see
|
||||
var owners = [];
|
||||
var infolog_grants = egw.grants(pushData.app);
|
||||
// Filter what's allowed down to those we care about
|
||||
var filtered = Object.keys(infolog_grants).filter(function (account) { return _this.state.owner.indexOf(account) >= 0; });
|
||||
// Check if we're interested in displaying by owner / responsible
|
||||
var owner_check = filtered.filter(function (value) {
|
||||
return pushData.acl.info_owner == value || pushData.acl.info_responsible.indexOf(value) >= 0;
|
||||
});
|
||||
if (!owner_check || owner_check.length == 0) {
|
||||
// The owner is not in the list of what we're allowed / care about
|
||||
// The owner is not in the list of what we care about
|
||||
return;
|
||||
}
|
||||
// Only need to update the list if we're on that view
|
||||
|
@ -465,9 +465,6 @@ class CalendarApp extends EgwApp
|
||||
*/
|
||||
push(pushData)
|
||||
{
|
||||
// Calendar cares about calendar & infolog
|
||||
if(pushData.app !== this.appname && pushData.app !== 'infolog') return;
|
||||
|
||||
switch (pushData.app)
|
||||
{
|
||||
case "calendar":
|
||||
@ -476,8 +473,17 @@ class CalendarApp extends EgwApp
|
||||
return super.push(pushData);
|
||||
}
|
||||
return this.push_calendar(pushData);
|
||||
case "infolog":
|
||||
return this.push_infolog(pushData);
|
||||
default:
|
||||
if(jQuery.extend([],egw.preference("integration_toggle","calendar")).indexOf(pushData.app) >= 0)
|
||||
{
|
||||
if(pushData.app == "infolog")
|
||||
{
|
||||
return this.push_infolog(pushData);
|
||||
}
|
||||
// Other integration here
|
||||
// TODO
|
||||
debugger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -488,20 +494,24 @@ class CalendarApp extends EgwApp
|
||||
*/
|
||||
private push_infolog(pushData : PushData)
|
||||
{
|
||||
// Check if we have access
|
||||
if(!this._push_grant_check(pushData, ["info_owner","info_responsible"],"infolog"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// check visibility - grants is ID => permission of people we're allowed to see
|
||||
let owners = [];
|
||||
let infolog_grants = egw.grants(pushData.app);
|
||||
|
||||
// Filter what's allowed down to those we care about
|
||||
let filtered = Object.keys(infolog_grants).filter(account => this.state.owner.indexOf(account) >= 0);
|
||||
|
||||
// Check if we're interested in displaying by owner / responsible
|
||||
let owner_check = filtered.filter(function(value) {
|
||||
return pushData.acl.info_owner == value || pushData.acl.info_responsible.indexOf(value) >= 0;
|
||||
})
|
||||
if(!owner_check || owner_check.length == 0)
|
||||
{
|
||||
// The owner is not in the list of what we're allowed / care about
|
||||
// The owner is not in the list of what we care about
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,6 @@ require("../jsapi/egw_global");
|
||||
require("../etemplate/et2_types");
|
||||
var egw_app_1 = require("../../api/js/jsapi/egw_app");
|
||||
var etemplate2_1 = require("../../api/js/etemplate/etemplate2");
|
||||
var et2_extension_nextmatch_1 = require("../../api/js/etemplate/et2_extension_nextmatch");
|
||||
var CRM_1 = require("../../addressbook/js/CRM");
|
||||
/**
|
||||
* UI for Infolog
|
||||
@ -49,6 +48,9 @@ var InfologApp = /** @class */ (function (_super) {
|
||||
var _this =
|
||||
// call parent
|
||||
_super.call(this, 'infolog') || this;
|
||||
// These fields help with push filtering & access control to see if we care about a push message
|
||||
_this.push_grant_fields = ["info_owner", "info_responsible"];
|
||||
_this.push_filter_fields = ["info_owner", "info_responsible"];
|
||||
_this._action_ids = [];
|
||||
_this._action_all = false;
|
||||
return _this;
|
||||
@ -154,96 +156,6 @@ var InfologApp = /** @class */ (function (_super) {
|
||||
this.et2._inst.refresh(_msg, _app, _id, _type);
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 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: ask server for data, add in intelligently
|
||||
* @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
|
||||
*/
|
||||
InfologApp.prototype.push = function (pushData) {
|
||||
var _this = this;
|
||||
if (pushData.app !== this.appname)
|
||||
return;
|
||||
// pushData does not contain everything, just the minimum.
|
||||
var event = pushData.acl || {};
|
||||
if (pushData.type === 'delete') {
|
||||
return _super.prototype.push.call(this, pushData);
|
||||
}
|
||||
// If we know about it and it's an update, just update.
|
||||
// This must be before all ACL checks, as responsible might have changed and entry need to be removed
|
||||
// (server responds then with null / no entry causing the entry to disapear)
|
||||
if (pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData))) {
|
||||
return this.et2.getInstanceManager().refresh("", pushData.app, pushData.id, pushData.type);
|
||||
}
|
||||
// check visibility - grants is ID => permission of people we're allowed to see
|
||||
if (typeof this._grants === 'undefined') {
|
||||
this._grants = egw.grants(this.appname);
|
||||
}
|
||||
// check user has a grant from owner or a responsible
|
||||
if (this._grants && typeof this._grants[pushData.acl.info_owner] === 'undefined' &&
|
||||
// responsible gets implicit access, so we need to check them too
|
||||
!pushData.acl.info_responsible.filter(function (res) { return typeof _this._grants[res] !== 'undefined'; }).length) {
|
||||
// No ACL access
|
||||
return;
|
||||
}
|
||||
// no responsible means, owner is responsible
|
||||
if (!pushData.acl.info_responsible || !pushData.acl.info_responsible.length) {
|
||||
pushData.acl.info_responsible = [pushData.acl.info_owner];
|
||||
}
|
||||
// Filter what's allowed down to those we care about
|
||||
var filters = {
|
||||
owner: { col: "info_owner", filter_values: [] },
|
||||
responsible: { col: "info_responsible", filter_values: [] }
|
||||
};
|
||||
if (this.et2) {
|
||||
this.et2.iterateOver(function (nm) {
|
||||
var value = nm.getValue();
|
||||
if (!value || !value.col_filter)
|
||||
return;
|
||||
for (var _i = 0, _a = Object.values(filters); _i < _a.length; _i++) {
|
||||
var field_filter = _a[_i];
|
||||
if (value.col_filter[field_filter.col]) {
|
||||
field_filter.filter_values.push(value.col_filter[field_filter.col]);
|
||||
}
|
||||
}
|
||||
}, this, et2_extension_nextmatch_1.et2_nextmatch);
|
||||
}
|
||||
var _loop_1 = function (field_filter) {
|
||||
// no filter set
|
||||
if (field_filter.filter_values.length == 0)
|
||||
return "continue";
|
||||
// acl value is a scalar (not array) --> check contained in filter
|
||||
if (pushData.acl && typeof pushData.acl[field_filter.col] !== 'object') {
|
||||
if (field_filter.filter_values.indexOf(pushData.acl[field_filter.col]) < 0) {
|
||||
return { value: void 0 };
|
||||
}
|
||||
return "continue";
|
||||
}
|
||||
// acl value is an array (eg. info_responsible) --> check intersection with filter
|
||||
if (!field_filter.filter_values.filter(function (account) { return pushData.acl[field_filter.col].indexOf(account) >= 0; }).length) {
|
||||
return { value: void 0 };
|
||||
}
|
||||
};
|
||||
// check filters against ACL data
|
||||
for (var _i = 0, _a = Object.values(filters); _i < _a.length; _i++) {
|
||||
var field_filter = _a[_i];
|
||||
var state_1 = _loop_1(field_filter);
|
||||
if (typeof state_1 === "object")
|
||||
return state_1.value;
|
||||
}
|
||||
// Pass actual refresh on to just nextmatch
|
||||
var nm = this.et2.getDOMWidgetById('nm');
|
||||
nm.refresh(pushData.id, pushData.type);
|
||||
};
|
||||
/**
|
||||
* Retrieve the current state of the application for future restoration
|
||||
*
|
||||
|
@ -31,8 +31,9 @@ import {CRMView} from "../../addressbook/js/CRM";
|
||||
class InfologApp extends EgwApp
|
||||
{
|
||||
|
||||
// Hold on to ACL grants
|
||||
private _grants : any;
|
||||
// These fields help with push filtering & access control to see if we care about a push message
|
||||
protected push_grant_fields = ["info_owner","info_responsible"];
|
||||
protected push_filter_fields = ["info_owner","info_responsible"]
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@ -163,108 +164,6 @@ class InfologApp extends EgwApp
|
||||
this.et2._inst.refresh(_msg, _app, _id, _type);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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: ask server for data, add in intelligently
|
||||
* @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)
|
||||
{
|
||||
if(pushData.app !== this.appname) return;
|
||||
|
||||
// pushData does not contain everything, just the minimum.
|
||||
let event = pushData.acl || {};
|
||||
|
||||
if(pushData.type === 'delete')
|
||||
{
|
||||
return super.push(pushData);
|
||||
}
|
||||
|
||||
// If we know about it and it's an update, just update.
|
||||
// This must be before all ACL checks, as responsible might have changed and entry need to be removed
|
||||
// (server responds then with null / no entry causing the entry to disapear)
|
||||
if (pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData)))
|
||||
{
|
||||
return this.et2.getInstanceManager().refresh("", pushData.app, pushData.id, pushData.type);
|
||||
}
|
||||
|
||||
// check visibility - grants is ID => permission of people we're allowed to see
|
||||
if (typeof this._grants === 'undefined')
|
||||
{
|
||||
this._grants = egw.grants(this.appname);
|
||||
}
|
||||
// check user has a grant from owner or a responsible
|
||||
if (this._grants && typeof this._grants[pushData.acl.info_owner] === 'undefined' &&
|
||||
// responsible gets implicit access, so we need to check them too
|
||||
!pushData.acl.info_responsible.filter(res => typeof this._grants[res] !== 'undefined').length)
|
||||
{
|
||||
// No ACL access
|
||||
return;
|
||||
}
|
||||
|
||||
// no responsible means, owner is responsible
|
||||
if (!pushData.acl.info_responsible || !pushData.acl.info_responsible.length)
|
||||
{
|
||||
pushData.acl.info_responsible = [pushData.acl.info_owner];
|
||||
}
|
||||
|
||||
// Filter what's allowed down to those we care about
|
||||
let filters = {
|
||||
owner: {col: "info_owner", filter_values: []},
|
||||
responsible: {col: "info_responsible", filter_values: []}
|
||||
};
|
||||
if(this.et2)
|
||||
{
|
||||
this.et2.iterateOver( function(nm) {
|
||||
let value = nm.getValue();
|
||||
if(!value || !value.col_filter) return;
|
||||
|
||||
for(let field_filter of Object.values(filters))
|
||||
{
|
||||
if(value.col_filter[field_filter.col])
|
||||
{
|
||||
field_filter.filter_values.push(value.col_filter[field_filter.col]);
|
||||
}
|
||||
}
|
||||
},this, et2_nextmatch);
|
||||
}
|
||||
|
||||
// check filters against ACL data
|
||||
for(let field_filter of Object.values(filters))
|
||||
{
|
||||
// no filter set
|
||||
if (field_filter.filter_values.length == 0) continue;
|
||||
|
||||
// acl value is a scalar (not array) --> check contained in filter
|
||||
if (pushData.acl && typeof pushData.acl[field_filter.col] !== 'object')
|
||||
{
|
||||
if (field_filter.filter_values.indexOf(pushData.acl[field_filter.col]) < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// acl value is an array (eg. info_responsible) --> check intersection with filter
|
||||
if(!field_filter.filter_values.filter(account => pushData.acl[field_filter.col].indexOf(account) >= 0).length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass actual refresh on to just nextmatch
|
||||
let nm = <et2_nextmatch>this.et2.getDOMWidgetById('nm');
|
||||
nm.refresh(pushData.id, pushData.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current state of the application for future restoration
|
||||
|
@ -30,7 +30,6 @@ require("jqueryui");
|
||||
require("../jsapi/egw_global");
|
||||
require("../etemplate/et2_types");
|
||||
var egw_app_1 = require("../../api/js/jsapi/egw_app");
|
||||
var etemplate2_1 = require("../../api/js/etemplate/etemplate2");
|
||||
/**
|
||||
* UI for timesheet
|
||||
*
|
||||
@ -39,7 +38,11 @@ var etemplate2_1 = require("../../api/js/etemplate/etemplate2");
|
||||
var TimesheetApp = /** @class */ (function (_super) {
|
||||
__extends(TimesheetApp, _super);
|
||||
function TimesheetApp() {
|
||||
return _super.call(this, 'timesheet') || this;
|
||||
var _this = _super.call(this, 'timesheet') || this;
|
||||
// These fields help with push filtering & access control to see if we care about a push message
|
||||
_this.push_grant_fields = ["ts_owner"];
|
||||
_this.push_filter_fields = ["ts_owner"];
|
||||
return _this;
|
||||
}
|
||||
/**
|
||||
* This function is called when the etemplate2 object is loaded
|
||||
@ -174,49 +177,6 @@ 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);
|
||||
}
|
||||
// This must be before all ACL checks, as owner might have changed and entry need to be removed
|
||||
// (server responds then with null / no entry causing the entry to disapear)
|
||||
if (pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData))) {
|
||||
return etemplate2_1.etemplate2.app_refresh("", pushData.app, pushData.id, pushData.type);
|
||||
}
|
||||
// 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.ts_owner] === '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 && ((_c = nm_value.col_filter) === null || _c === void 0 ? void 0 : _c.ts_owner) && nm_value.col_filter.ts_owner != pushData.acl.ts_owner) {
|
||||
return;
|
||||
}
|
||||
etemplate2_1.etemplate2.app_refresh("", pushData.app, pushData.id, pushData.type);
|
||||
};
|
||||
/**
|
||||
* Run action via ajax
|
||||
*
|
||||
|
@ -29,6 +29,10 @@ import {etemplate2} from "../../api/js/etemplate/etemplate2";
|
||||
class TimesheetApp extends EgwApp
|
||||
{
|
||||
|
||||
// These fields help with push filtering & access control to see if we care about a push message
|
||||
protected push_grant_fields = ["ts_owner"];
|
||||
protected push_filter_fields = ["ts_owner"]
|
||||
|
||||
constructor()
|
||||
{
|
||||
super('timesheet');
|
||||
@ -199,58 +203,6 @@ class TimesheetApp extends EgwApp
|
||||
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);
|
||||
}
|
||||
|
||||
// This must be before all ACL checks, as owner might have changed and entry need to be removed
|
||||
// (server responds then with null / no entry causing the entry to disapear)
|
||||
if (pushData.type !== "add" && this.egw.dataHasUID(this.uid(pushData)))
|
||||
{
|
||||
return etemplate2.app_refresh("", pushData.app, pushData.id, pushData.type);
|
||||
}
|
||||
|
||||
// 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.ts_owner] === 'undefined') return;
|
||||
|
||||
// check if we might not see it because of an owner filter
|
||||
let nm = <et2_nextmatch>this.et2?.getWidgetById('nm');
|
||||
let nm_value = nm?.getValue();
|
||||
if (nm && nm_value && nm_value.col_filter?.ts_owner && nm_value.col_filter.ts_owner != pushData.acl.ts_owner)
|
||||
{
|
||||
return;
|
||||
}
|
||||
etemplate2.app_refresh("",pushData.app, pushData.id, pushData.type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run action via ajax
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user