* Calendar: New context menu action to manually [re]send notifications

This commit is contained in:
nathangray 2021-02-09 13:42:51 -07:00
parent dcf7b1a1d2
commit 2005a67b3c
14 changed files with 221 additions and 90 deletions

View File

@ -851,9 +851,10 @@ class calendar_boupdate extends calendar_bo
* @param array $new_event =null Event after the change * @param array $new_event =null Event after the change
* @param int|string $user =0 User/participant who started the notify, default current user * @param int|string $user =0 User/participant who started the notify, default current user
* @param array $alarm =null values for "offset", "start", etc. * @param array $alarm =null values for "offset", "start", etc.
* @parqm boolean $ignore_prefs Ignore the user's preferences about when they want to be notified and send it
* @return bool true/false * @return bool true/false
*/ */
function _send_update($msg_type, $to_notify, $old_event, $new_event=null, $user=0, array $alarm=null) function _send_update($msg_type, $to_notify, $old_event, $new_event=null, $user=0, array $alarm=null, $ignore_prefs = false)
{ {
//error_log(__METHOD__."($msg_type,".array2string($to_notify).",...) ".array2string($new_event)); //error_log(__METHOD__."($msg_type,".array2string($to_notify).",...) ".array2string($new_event));
if (!is_array($to_notify)) if (!is_array($to_notify))
@ -1035,7 +1036,7 @@ class calendar_boupdate extends calendar_bo
$fullname = $res_info && !empty($res_info['name']) ? $res_info['name'] : $userid; $fullname = $res_info && !empty($res_info['name']) ? $res_info['name'] : $userid;
} }
$m_type = $msg_type; $m_type = $msg_type;
if (!self::update_requested($userid, $part_prefs, $m_type, $old_event, $new_event, $role, if (!$ignore_prefs && !self::update_requested($userid, $part_prefs, $m_type, $old_event, $new_event, $role,
$event['participants'][$GLOBALS['egw_info']['user']['account_id']])) $event['participants'][$GLOBALS['egw_info']['user']['account_id']]))
{ {
continue; continue;

View File

@ -42,6 +42,7 @@ class calendar_uiforms extends calendar_ui
'cat_acl' => true, 'cat_acl' => true,
'meeting' => true, 'meeting' => true,
'mail_import' => true, 'mail_import' => true,
'notify' => true
); );
/** /**
@ -1773,90 +1774,14 @@ class calendar_uiforms extends calendar_ui
$content['duration'] = $content['end'] - $content['start']; $content['duration'] = $content['end'] - $content['start'];
if (isset($this->durations[$content['duration']])) $content['end'] = ''; if (isset($this->durations[$content['duration']])) $content['end'] = '';
$row = 3;
$readonlys = $content['participants'] = $preserv['participants'] = array(); $readonlys = $content['participants'] = $preserv['participants'] = array();
// preserve some ui elements, if set eg. under error-conditions // preserve some ui elements, if set eg. under error-conditions
foreach(array('quantity','resource','role') as $n) foreach(array('quantity','resource','role') as $n)
{ {
if (isset($event['participants'][$n])) $content['participants'][$n] = $event['participants'][$n]; if (isset($event['participants'][$n])) $content['participants'][$n] = $event['participants'][$n];
} }
foreach($event['participant_types'] as $type => $participants) $this->setup_participants($event,$content,$readonlys,$preserv,$view);
{
$name = 'accounts';
if (isset($this->bo->resources[$type]))
{
$name = $this->bo->resources[$type]['app'];
}
// sort participants (in there group/app) by title
uksort($participants, array($this, 'uid_title_cmp'));
foreach($participants as $id => $status)
{
$uid = $type == 'u' ? $id : $type.$id;
$quantity = $role = null;
calendar_so::split_status($status,$quantity,$role);
$preserv['participants'][$row] = $content['participants'][$row] = array(
'app' => $name == 'accounts' ? ($GLOBALS['egw']->accounts->get_type($id) == 'g' ? 'Group' : 'User') : $name,
'uid' => $uid,
'status' => $status,
'old_status' => $status,
'quantity' => $quantity > 1 || $uid[0] == 'r' ? $quantity : '', // only display quantity for resources or if > 1
'role' => $role,
);
// replace iCal roles with a nicer label and remove regular REQ-PARTICIPANT
if (isset($this->bo->roles[$role]))
{
$content['participants'][$row]['role_label'] = lang($this->bo->roles[$role]);
}
// allow third party apps to use categories for roles
elseif(substr($role,0,6) == 'X-CAT-')
{
$content['participants'][$row]['role_label'] = $GLOBALS['egw']->categories->id2name(substr($role,6));
}
else
{
$content['participants'][$row]['role_label'] = lang(str_replace('X-','',$role));
}
$content['participants'][$row]['delete_id'] = strpbrk($uid,'"\'<>') !== false ? md5($uid) : $uid;
//echo "<p>$uid ($quantity): $role --> {$content['participants'][$row]['role']}</p>\n";
if (($no_status = !$this->bo->check_status_perms($uid,$event)) || $view)
$readonlys['participants'][$row]['status'] = $no_status;
if ($preserv['hide_delete'] || !$this->bo->check_perms(Acl::EDIT,$event))
$readonlys['participants']['delete'][$uid] = true;
// todo: make the participants available as links with email as title
$content['participants'][$row++]['title'] = $this->get_title($uid);
// enumerate group-invitations, so people can accept/reject them
if ($name == 'accounts' && $GLOBALS['egw']->accounts->get_type($id) == 'g' &&
($members = $GLOBALS['egw']->accounts->members($id,true)))
{
$sel_options['status']['G'] = lang('Select one');
// sort members by title
usort($members, array($this, 'uid_title_cmp'));
foreach($members as $member)
{
if (!isset($participants[$member]) && $this->bo->check_perms(Acl::READ,0,$member))
{
$preserv['participants'][$row] = $content['participants'][$row] = array(
'app' => 'Group invitation',
'uid' => $member,
'status' => 'G',
);
$readonlys['participants'][$row]['quantity'] = $readonlys['participants']['delete'][$member] = true;
// read access is enough to invite participants, but you need edit rights to change status
$readonlys['participants'][$row]['status'] = !$this->bo->check_perms(Acl::EDIT,0,$member);
$content['participants'][$row++]['title'] = Api\Accounts::username($member);
}
}
}
}
// resouces / apps we shedule, atm. resources and addressbook
$content['participants']['cal_resources'] = '';
foreach($this->bo->resources as $data)
{
if ($data['app'] == 'email') continue; // make no sense, as we cant search for email
$content['participants']['cal_resources'] .= ','.$data['app'];
}
}
$content['participants']['status_date'] = $preserv['actual_date']; $content['participants']['status_date'] = $preserv['actual_date'];
// set notify_externals in participants from cfs // set notify_externals in participants from cfs
if (!empty($event['##notify_externals'])) if (!empty($event['##notify_externals']))
@ -2045,6 +1970,98 @@ class calendar_uiforms extends calendar_ui
} }
} }
/**
* Set up the participants for display in edit dialog
*
* @param $event
* @param $content
* @param $readonlys
* @param $preserv
* @param $view
*/
protected function setup_participants($event, &$content, &$readonlys, &$preserv, $view)
{
$row = 3;
foreach($event['participant_types'] as $type => $participants)
{
$name = 'accounts';
if (isset($this->bo->resources[$type]))
{
$name = $this->bo->resources[$type]['app'];
}
// sort participants (in there group/app) by title
uksort($participants, array($this, 'uid_title_cmp'));
foreach($participants as $id => $status)
{
$uid = $type == 'u' ? $id : $type.$id;
$quantity = $role = null;
calendar_so::split_status($status,$quantity,$role);
$preserv['participants'][$row] = $content['participants'][$row] = array(
'app' => $name == 'accounts' ? ($GLOBALS['egw']->accounts->get_type($id) == 'g' ? 'Group' : 'User') : $name,
'uid' => $uid,
'status' => $status,
'old_status' => $status,
'quantity' => $quantity > 1 || $uid[0] == 'r' ? $quantity : '', // only display quantity for resources or if > 1
'role' => $role,
);
// replace iCal roles with a nicer label and remove regular REQ-PARTICIPANT
if (isset($this->bo->roles[$role]))
{
$content['participants'][$row]['role_label'] = lang($this->bo->roles[$role]);
}
// allow third party apps to use categories for roles
elseif(substr($role,0,6) == 'X-CAT-')
{
$content['participants'][$row]['role_label'] = $GLOBALS['egw']->categories->id2name(substr($role,6));
}
else
{
$content['participants'][$row]['role_label'] = lang(str_replace('X-','',$role));
}
$content['participants'][$row]['delete_id'] = strpbrk($uid,'"\'<>') !== false ? md5($uid) : $uid;
//echo "<p>$uid ($quantity): $role --> {$content['participants'][$row]['role']}</p>\n";
if (($no_status = !$this->bo->check_status_perms($uid,$event)) || $view)
$readonlys['participants'][$row]['status'] = $no_status;
if ($preserv['hide_delete'] || !$this->bo->check_perms(Acl::EDIT,$event))
$readonlys['participants']['delete'][$uid] = true;
// todo: make the participants available as links with email as title
$content['participants'][$row++]['title'] = $this->get_title($uid);
// enumerate group-invitations, so people can accept/reject them
if ($name == 'accounts' && $GLOBALS['egw']->accounts->get_type($id) == 'g' &&
($members = $GLOBALS['egw']->accounts->members($id,true)))
{
$sel_options['status']['G'] = lang('Select one');
// sort members by title
usort($members, array($this, 'uid_title_cmp'));
foreach($members as $member)
{
if (!isset($participants[$member]) && $this->bo->check_perms(Acl::READ,0,$member))
{
$preserv['participants'][$row] = $content['participants'][$row] = array(
'app' => 'Group invitation',
'uid' => $member,
'status' => 'G',
);
$readonlys['participants'][$row]['quantity'] = $readonlys['participants']['delete'][$member] = true;
// read access is enough to invite participants, but you need edit rights to change status
$readonlys['participants'][$row]['status'] = !$this->bo->check_perms(Acl::EDIT,0,$member);
$content['participants'][$row++]['title'] = Api\Accounts::username($member);
}
}
}
}
// resouces / apps we shedule, atm. resources and addressbook
$content['participants']['cal_resources'] = '';
foreach($this->bo->resources as $data)
{
if ($data['app'] == 'email') continue; // make no sense, as we cant search for email
$content['participants']['cal_resources'] .= ','.$data['app'];
}
}
}
/** /**
* Remove (shared) lock via ajax, when edit popup get's closed * Remove (shared) lock via ajax, when edit popup get's closed
* *
@ -3464,4 +3481,35 @@ class calendar_uiforms extends calendar_ui
return $this->process_edit($event); return $this->process_edit($event);
} }
public function notify($content=array())
{
if(is_array($content) && $content['button'])
{
$participants = array_filter($content['participants']['notify']);
$this->bo->_send_update(MSG_ALARM,$participants,$content,null,0,null,true);
Framework::window_close();
}
list($id, $date) = explode(':',$_GET['id']);
$content = array();
$event = $this->bo->read($id, $date);
$this->setup_participants($event, $content, $readonlys,$preserve,true);
$content = array_merge($event, $content);
$sel_options = array(
'recur_type' => &$this->bo->recur_types,
'status' => $this->bo->verbose_status,
'duration' => $this->durations,
'role' => $this->bo->roles
);
$readonlys = [];
$etpl = new Etemplate('calendar.notify_dialog');
$preserve = $content;
$etpl->exec('calendar.calendar_uiforms.notify', $content, $sel_options, $readonlys, $preserve,2);
}
} }

View File

@ -963,6 +963,15 @@ class calendar_uilist extends calendar_ui
'hint' => 'Do not notify of these changes', 'hint' => 'Do not notify of these changes',
'group' => $group, 'group' => $group,
), ),
'notifications' => array(
'caption' => 'Send notifications',
'hint' => 'Send notifications to users right now',
'url' => 'calendar.calendar_uiforms.notify&id=$app_id',
'popup' => Link::get_registry('calendar', 'view_popup'),
'allowOnMultiple' => false,
'group' => $group,
'disableClass' => 'rowNoView',
)
); );
$status = array_map('lang',$this->bo->verbose_status); $status = array_map('lang',$this->bo->verbose_status);
unset($status['G']); unset($status['G']);

View File

@ -911,6 +911,7 @@ class calendar_uiviews extends calendar_ui
$actions['copy']['onExecute'] = 'javaScript:app.calendar.action_open'; $actions['copy']['onExecute'] = 'javaScript:app.calendar.action_open';
$actions['print']['open'] = '{"app": "calendar", "type": "edit", "extra": "cal_id=$id&print=1"}'; $actions['print']['open'] = '{"app": "calendar", "type": "edit", "extra": "cal_id=$id&print=1"}';
$actions['print']['onExecute'] = 'javaScript:app.calendar.action_open'; $actions['print']['onExecute'] = 'javaScript:app.calendar.action_open';
$actions['notifications']['onExecute'] = 'javaScript:app.calendar.action_open';
foreach($actions['status']['children'] as $id => &$status) foreach($actions['status']['children'] as $id => &$status)
{ {

View File

@ -13,6 +13,7 @@ var __extends = (this && this.__extends) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.listview = exports.planner = exports.month = exports.weekN = exports.week = exports.day4 = exports.day = exports.View = void 0;
var View = /** @class */ (function () { var View = /** @class */ (function () {
function View() { function View() {
} }

View File

@ -1650,9 +1650,10 @@ var CalendarApp = /** @class */ (function (_super) {
} }
else if (_action.data.url) { else if (_action.data.url) {
var url = _action.data.url; var url = _action.data.url;
url = url.replace(/(\$|%24)app/, app).replace(/(\$|%24)app_id/, app_id) url = url.replace(/(\$|%24)app_id/, app_id)
.replace(/(\$|%24)app/, app)
.replace(/(\$|%24)id/, id); .replace(/(\$|%24)id/, id);
this.egw.open_link(url); this.egw.open_link(url, _action.data.target, _action.data.popup);
} }
}; };
/** /**
@ -3572,8 +3573,7 @@ var CalendarApp = /** @class */ (function (_super) {
// Dates are user time, but we told javascript it was UTC. // Dates are user time, but we told javascript it was UTC.
// Send just the timestamp (as a string) with no timezone // Send just the timestamp (as a string) with no timezone
(typeof _data.start != "string" ? _data.start.toJSON() : _data.start).slice(0, -1), (typeof _data.start != "string" ? _data.start.toJSON() : _data.start).slice(0, -1),
(typeof _data.end != "string" ? _data.end.toJSON() : _data.end).slice(0, -1), (typeof _data.end != "string" ? _data.end.toJSON() : _data.end).slice(0, -1), {
{
participants: Object.keys(_data.participants).filter(function (v) { return v.match(/^[0-9]/); }) participants: Object.keys(_data.participants).filter(function (v) { return v.match(/^[0-9]/); })
}], function (_value) { }], function (_value) {
if (_value) { if (_value) {

View File

@ -1795,9 +1795,10 @@ class CalendarApp extends EgwApp
else if (_action.data.url) else if (_action.data.url)
{ {
var url = _action.data.url; var url = _action.data.url;
url = url.replace(/(\$|%24)app/,app).replace(/(\$|%24)app_id/,app_id) url = url.replace(/(\$|%24)app_id/,app_id)
.replace(/(\$|%24)id/,id); .replace(/(\$|%24)app/,app)
this.egw.open_link(url); .replace(/(\$|%24)id/,id);
this.egw.open_link(url, _action.data.target, _action.data.popup);
} }
} }

View File

@ -22,6 +22,7 @@ var __extends = (this && this.__extends) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.et2_calendar_event = void 0;
/*egw:uses /*egw:uses
/etemplate/js/et2_core_valueWidget; /etemplate/js/et2_core_valueWidget;
*/ */
@ -722,7 +723,6 @@ var et2_calendar_event = /** @class */ (function (_super) {
* @private * @private
*/ */
et2_calendar_event.prototype._status_check = function (event, filter, owner) { et2_calendar_event.prototype._status_check = function (event, filter, owner) {
var _a;
if (!owner || !event) { if (!owner || !event) {
return false; return false;
} }
@ -756,7 +756,7 @@ var et2_calendar_event = /** @class */ (function (_super) {
return element.id == owner; return element.id == owner;
}) || {}; }) || {};
var matching_participant = typeof resource.resources == "undefined" ? var matching_participant = typeof resource.resources == "undefined" ?
resource : (_a = resource) === null || _a === void 0 ? void 0 : _a.resources.filter(function (id) { return typeof event.participants[id] != "undefined"; }); resource : resource === null || resource === void 0 ? void 0 : resource.resources.filter(function (id) { return typeof event.participants[id] != "undefined"; });
if (matching_participant.length > 0) { if (matching_participant.length > 0) {
return this._status_check(event, filter, matching_participant); return this._status_check(event, filter, matching_participant);
} }

View File

@ -22,6 +22,7 @@ var __extends = (this && this.__extends) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.et2_calendar_owner = void 0;
/*egw:uses /*egw:uses
et2_widget_taglist; et2_widget_taglist;
*/ */

View File

@ -22,6 +22,7 @@ var __extends = (this && this.__extends) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.et2_calendar_planner = void 0;
/*egw:uses /*egw:uses
/calendar/js/et2_widget_view.js; /calendar/js/et2_widget_view.js;
/calendar/js/et2_widget_planner_row.js; /calendar/js/et2_widget_planner_row.js;

View File

@ -22,6 +22,7 @@ var __extends = (this && this.__extends) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.et2_calendar_planner_row = void 0;
/*egw:uses /*egw:uses
/calendar/js/et2_widget_view.js; /calendar/js/et2_widget_view.js;
/calendar/js/et2_widget_daycol.js; /calendar/js/et2_widget_daycol.js;
@ -372,7 +373,7 @@ var et2_calendar_planner_row = /** @class */ (function (_super) {
get: function () { get: function () {
return this._date_helper; return this._date_helper;
}, },
enumerable: true, enumerable: false,
configurable: true configurable: true
}); });
/** /**

View File

@ -22,6 +22,7 @@ var __extends = (this && this.__extends) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.et2_calendar_timegrid = void 0;
/*egw:uses /*egw:uses
/calendar/js/et2_widget_view.js; /calendar/js/et2_widget_view.js;
*/ */

View File

@ -22,6 +22,7 @@ var __extends = (this && this.__extends) || (function () {
}; };
})(); })();
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.et2_calendar_view = void 0;
var et2_core_valueWidget_1 = require("../../api/js/etemplate/et2_core_valueWidget"); var et2_core_valueWidget_1 = require("../../api/js/etemplate/et2_core_valueWidget");
var et2_core_inheritance_1 = require("../../api/js/etemplate/et2_core_inheritance"); var et2_core_inheritance_1 = require("../../api/js/etemplate/et2_core_inheritance");
/** /**
@ -278,7 +279,7 @@ var et2_calendar_view = /** @class */ (function (_super) {
get: function () { get: function () {
return this._date_helper; return this._date_helper;
}, },
enumerable: true, enumerable: false,
configurable: true configurable: true
}); });
et2_calendar_view.prototype._createNamespace = function () { et2_calendar_view.prototype._createNamespace = function () {

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
<overlay>
<template id="calendar.notify_dialog" template="" lang="" group="0" version="1.9.001">
<grid width="100%">
<columns>
<column width="88"/>
<column width="130"/>
<column width="88"/>
<column width="130"/>
<column width="130"/>
</columns>
<rows>
<row class="dialogHeader" height="28">
<description id="title" class="et2_fullWidth" span="4"/>
<hbox>
<description font_style="n" id="id"/>
<appicon/>
</hbox>
</row>
<row class="dialogHeader2" height="28" >
<description for="start" value="Start" width="88"/>
<date-time id="start" readonly="true"/>
<description for="duration" value="Duration" id="calendar_edit_duration" />
<select statustext="Duration of the meeting" class="et2_fullWidth" id="duration" no_lang="1" readonly="true" options="Use end date,,,,,,,false"/>
<date-time id="end" readonly="true"/>
</row>
</rows>
</grid>
<grid width="100%" id="participants">
<columns>
<column width="85"/>
<column width="350"/>
<column width="70"/>
<column width="100"/>
<column/>
</columns>
<rows>
<row></row>
<row></row>
<row class="th thb">
<description value="Type"/>
<description value="Participants"/>
<description value="Role"/>
<description value="Status"/>
<description/>
</row>
<row valign="top">
<description id="${row}[app]"/>
<description id="${row}[title]" no_lang="1"/>
<description id="${row}[role_label]"/>
<select id="${row}[status]" readonly="true"/>
<checkbox align="center" label="Notify" id="notify[$row_cont[delete_id]]"/>
</row>
</rows>
</grid>
<hbox class="dialogFooterToolbar">
<button label="OK" id="button[save]" image="mail" background_image="1"/>
<button statustext="Close the window" label="Cancel" id="button[cancel]" onclick="window.close();" image="cancel" background_image="1"/>
</hbox>
<styles>
.selectRole select { width: 100%; }
</styles>
</template>
</overlay>