* Calendar/Mail: improved display of meeting requests: what's changed, single recurrence or whole series, display and enter comment when accepting/rejecting a request

This commit is contained in:
ralf 2024-07-06 19:39:21 +02:00
parent 8a984c46ea
commit c37fd3e380
6 changed files with 506 additions and 431 deletions

View File

@ -900,6 +900,7 @@ class calendar_boupdate extends calendar_bo
*/
function _send_update($msg_type, $to_notify, $old_event, $new_event=null, $user=0, ?array $alarm=null, $ignore_prefs = false)
{
try {
//error_log(__METHOD__."($msg_type, ".json_encode($to_notify).", ..., ".json_encode($new_event).", ...)");
if (!is_array($to_notify))
{
@ -1002,11 +1003,11 @@ class calendar_boupdate extends calendar_bo
$enddate = new Api\DateTime($event['end'], new DateTimeZone($user_prefs['common']['tz']));
$modified = new Api\DateTime($event['modified'], new DateTimeZone($user_prefs['common']['tz']));
if ($old_event) $olddate = new Api\DateTime($old_event['start'], new DateTimeZone($user_prefs['common']['tz']));
$rdates = array_map(static function($rdate) use ($user_prefs)
{
$rdates = array_map(static function ($rdate) use ($user_prefs) {
return new Api\DateTime($rdate, new DateTimeZone($user_prefs['common']['tz']));
}, $event['recur_rdates'] ?? []);
$recur_date = isset($event['recur_date']) ? new Api\DateTime($event['recur_date'], new DateTimeZone($user_prefs['common']['tz'])) : null;
$recurrence = isset($event['recurrence']) ? new Api\DateTime($event['recurrence'], new DateTimeZone($user_prefs['common']['tz'])) : null;
//error_log(__METHOD__."() date_default_timezone_get()=".date_default_timezone_get().", user-timezone=".Api\DateTime::$user_timezone->getName().", startdate=".$startdate->format().", enddate=".$enddate->format().", updated=".$modified->format().", olddate=".($olddate ? $olddate->format() : ''));
$owner_prefs = $ics = null;
@ -1119,8 +1120,7 @@ class calendar_boupdate extends calendar_bo
try
{
$timezone = new DateTimeZone($part_prefs['common']['tz']);
}
catch(Exception $e)
} catch (Exception $e)
{
$timezone = new DateTimeZone($GLOBALS['egw_info']['server']['server_timezone']);
}
@ -1149,8 +1149,7 @@ class calendar_boupdate extends calendar_bo
$details['updated'] = $modified->format($timeformat) . ', ' . Api\Accounts::username($event['modifier']);
// we also need to "fix" timezone for rdates, to not get wrong times!
$cleared_event['recur_rdates'] = array_map(static function ($rdate) use ($timezone)
{
$cleared_event['recur_rdates'] = array_map(static function ($rdate) use ($timezone) {
return $rdate->setTimezone($timezone);
}, $rdates);
@ -1159,6 +1158,10 @@ class calendar_boupdate extends calendar_bo
$cleared_event['recur_date'] = $recur_date->setTimezone($timezone);
$details['recur_date'] = $recur_date->format($timeformat);
}
if (isset($recurrence))
{
$cleared_event['recurrence'] = $recurrence->setTimezone($timezone);
}
// Current date doesn't need to go into the cleared event, just for details
$date->setTimezone($timezone);
@ -1185,7 +1188,9 @@ class calendar_boupdate extends calendar_bo
'account_id' => $userid,
'cal_id' => $details['id'],
'notify_only' => true
], ['participants' =>array_filter($event['participants'], function($key){return is_numeric($key);}, ARRAY_FILTER_USE_KEY)], $startdate, $enddate);
], ['participants' => array_filter($event['participants'], function ($key) {
return is_numeric($key);
}, ARRAY_FILTER_USE_KEY)], $startdate, $enddate);
$event_arr['videoconference'] = [
'field' => lang('Video Conference'),
'data' => $details['videoconference'],
@ -1250,7 +1255,8 @@ class calendar_boupdate extends calendar_bo
// send via notification_app
if ($GLOBALS['egw_info']['apps']['notifications']['enabled'])
{
try {
try
{
//error_log(__METHOD__."() notifying $userid from $senderid: $subject");
$notification = new notifications();
$notification->set_receivers(array($userid));
@ -1296,11 +1302,14 @@ class calendar_boupdate extends calendar_bo
$notification->set_popupmessage($subject . "\n\n" . $notify_body . "\n\n" . $details['description'] . "\n\n" . $details_body . "\n\n");
$notification->set_popuplinks(array($details['link_arr'] + array('app' => 'calendar')));
if(!empty($attachment)) { $notification->set_attachments(array($attachment)); }
if (!empty($attachment))
{
$notification->set_attachments(array($attachment));
}
$notification->send();
$errors = notifications::errors(true);
}
catch (Exception $exception) {
} catch (Exception $exception)
{
$errors = [$exception->getMessage()];
continue;
}
@ -1339,7 +1348,26 @@ class calendar_boupdate extends calendar_bo
}
// restore timezone, in case we had to reset it to server-timezone
if (!empty($restore_tz)) date_default_timezone_set($restore_tz);
}
catch (Throwable $e) {
// logging all exceptions and errors to the error_log AND pushing them to user
$message = null;
_egw_log_exception($e,$message);
$response = new Api\Json\Push($GLOBALS['egw_info']['user']['account_id']);
$message .= ($message ? "\n\n" : '').$e->getMessage();
$message .= "\n\n".$e->getFile().' ('.$e->getLine().')';
// only show trace (incl. function arguments) if explicitly enabled, eg. on a development system
if ($GLOBALS['egw_info']['server']['exception_show_trace'])
{
$message .= "\n\n".$e->getTraceAsString();
}
$response->message($message, 'error');
// under PHP 8 the destructor is called to late and the response is not send
$GLOBALS['egw']->__destruct();
exit;
}
return true;
}
@ -1844,9 +1872,10 @@ class calendar_boupdate extends calendar_bo
* DEPRECATED: we always (have to) update timestamp, as they are required for sync!
* @param boolean|"NOPUSH" $skip_notification =false true: send NO notifications, default false = send them,
* "NOPUSH": also do NOT send push notifications / call Link::notifiy(), which still happens for true
* @param ?string $comment Comment to send with notification to organizer (as iCal COMMENT)
* @return int number of changed recurrences
*/
function set_status($event,$uid,$status,$recur_date=0,$ignore_acl=false,$updateTS=true,$skip_notification=false)
function set_status($event,$uid,$status,$recur_date=0,$ignore_acl=false,$updateTS=true,$skip_notification=false, ?string $comment=null)
{
unset($updateTS);
@ -1893,6 +1922,7 @@ class calendar_boupdate extends calendar_bo
{
if (!is_array($event)) $event = $this->read($cal_id);
if (isset($recur_date)) $event = $this->read($event['id'],$recur_date); //re-read the actually edited recurring event
if (!empty($comment)) $event['comment'] = $comment;
$user_id = is_numeric($uid) ? (int)$uid : $uid;
$this->send_update($status2msg[$status],$event['participants'],$event, null, $user_id);
}

View File

@ -231,6 +231,7 @@ class calendar_ical extends calendar_boupdate
'SEQUENCE' => 'etag',
'STATUS' => 'status',
'ATTACH' => 'attachments',
'COMMENT' => 'comment',
);
if (!is_array($this->supportedFields)) $this->setSupportedFields();
@ -2236,6 +2237,7 @@ class calendar_ical extends calendar_boupdate
'recurrence' => 'recurrence',
'etag' => 'etag',
'status' => 'status',
'comment' => 'comment',
);
@ -3119,6 +3121,10 @@ class calendar_ical extends calendar_boupdate
$event['##videoconference'] = $attributes['value'];
break;
case 'COMMENT': // used e.g. at mailbox.org as comment in REPLYs send to the organize
$event['comment'] = $attributes['value'];
break;
// ignore all PROPS, we dont want to store like X-properties or unsupported props
case 'DTSTAMP':
case 'SEQUENCE':

View File

@ -2247,6 +2247,10 @@ class calendar_uiforms extends calendar_ui
{
$event['sender_warning'] = lang('The sender "%1" is NOT the extern organizer "%2", proceed with caution!', $ical_sender, $organizer);
}
if ($event['recurrence'])
{
$master = $this->bo->read($event['uid']);
}
switch(strtolower($ical_method))
{
case 'reply':
@ -2262,7 +2266,7 @@ class calendar_uiforms extends calendar_ui
{
$event['sender_warning'] = lang('The sender "%1" is NOT the participant replying "%2", proceed with caution!', $ical_sender, $participant);
}
if ($event['ical_sender_uid'] && $this->bo->check_status_perms($event['ical_sender_uid'], $existing_event))
if ($event['ical_sender_uid'])
{
$existing_status = $existing_event['participants'][$event['ical_sender_uid']];
// check if email matches, in case we have now something like "Name <email>"
@ -2287,31 +2291,41 @@ class calendar_uiforms extends calendar_ui
calendar_so::split_status($existing_status, $quantity, $role);
if ($existing_status != $event['ical_sender_status'])
{
$readonlys['button[apply]'] = false;
$readonlys['button[apply]'] = !$this->bo->check_status_perms($event['ical_sender_uid'], $existing_event);
}
else
{
$event['error'] = lang('Status already applied');
$event['meeting-request-status'] = lang('Status already applied');
}
}
break;
case 'request':
case 'add':
$status = $existing_event['participants'][$user];
calendar_so::split_status($status, $quantity, $role);
if (!empty($extern_organizer) && self::event_changed($event, $existing_event))
if (($changed = self::event_changed($event, $existing_event)))
{
$event['error'] = lang('The extern organizer changed the event!',);
if (!empty($extern_organizer))
{
$event['error'] = lang('The extern organizer changed the event!');
$readonlys['button[apply]'] = false;
}
elseif (isset($existing_event['participants'][$user]) &&
$status != 'U' && isset($this->bo->verbose_status[$status]))
{
$event['error'] = lang('You already replied to this invitation with').': '.lang($this->bo->verbose_status[$status]);
}
else
{
$event['error'] = lang('Using already existing event on server.');
$event['error'] = lang('The organizer changed the event!');
}
//$event['error'] .= ' '.json_encode($changed);
$event['changed'] = array_combine(array_keys($changed), array_fill(0, count($changed), 'meetingRequestChanged'));
}
if (isset($existing_event['participants'][$user]) &&
$status != 'U' && isset($this->bo->verbose_status[$status]))
{
$event['meeting-request-status'] = lang('You already replied to this invitation with').': '.lang($this->bo->verbose_status[$status]);
}
else
{
$event['meeting-request-status'] = lang('Using already existing event on server.');
}
$user_and_memberships = $GLOBALS['egw']->accounts->memberships($user, true);
$user_and_memberships[] = $user;
@ -2354,7 +2368,7 @@ class calendar_uiforms extends calendar_ui
$status && $status !== 'X' ? $status : 'U'; // X --> no status given --> U = unknown
}
//error_log(__METHOD__."(...) parsed as ".array2string($event));
$event['recure'] = $this->bo->recure2string($event);
$event['recure'] = $this->bo->recure2string($master ?? null ?: $event);
$event['all_participants'] = implode(",\n",$this->bo->participants($event, true));
// EGroupware event has been deleted, don't let user resurrect it by accepting again
@ -2371,8 +2385,7 @@ class calendar_uiforms extends calendar_ui
(!empty($event['sender_warning']) ? "\n\n".$event['sender_warning'] : '');
}
// ignore events in the past (for recurring events check enddate!)
elseif ($this->bo->date2ts($event['start']) < $this->bo->now_su &&
(!$event['recur_type'] || $event['recur_enddate'] && $event['recur_enddate'] < $this->bo->now_su))
elseif (!$this->bo->eventInFuture($event))
{
$event['error'] = lang('Requested meeting is in the past!');
$readonlys['button[accept]'] = $readonlys['button[tentativ]'] =
@ -2440,7 +2453,7 @@ class calendar_uiforms extends calendar_ui
switch($button)
{
case 'reject':
if (!$event['id'])
if (empty($event['id']))
{
// send reply to organizer
$this->bo->send_update(MSG_REJECTED,array('e'.$event['organizer'] => 'DCHAIR'), $event);
@ -2476,7 +2489,7 @@ class calendar_uiforms extends calendar_ui
$event['id'] = $event['old']['id'];
}
// set status and send notification / meeting response
if ($this->bo->set_status($event['id'], $user, $status, $event['recurrence']))
if ($this->bo->set_status($event['id'], $user, $status, $event['recurrence'], false, true, false, $event['comment']))
{
$msg[] = lang('Status changed');
}
@ -2520,6 +2533,9 @@ class calendar_uiforms extends calendar_ui
case 'cancel':
$event['ics_method_label'] = lang('Meeting canceled');
break;
case 'add':
$event['ics_method_label'] = lang('Meeting request additional recurrence(s)');
break;
case 'request':
default:
$event['ics_method_label'] = lang('Meeting request');
@ -2540,12 +2556,12 @@ class calendar_uiforms extends calendar_ui
*
* @param array& $_event invitation, on return user status changed to the one from old $old
* @param array $_old existing event on server
* @return boolean true if there are some changes, false if not
* @return array with changed event attributes plus "recur" with any "recur_*" changed, empty array if nothing changed
*/
function event_changed(array &$_event, array $_old)
{
static $keys_to_check = array('start', 'end', 'title', 'description', 'location', 'participants',
'recur_type', 'recur_data', 'recur_interval', 'recur_exception');
'recur_type', 'recur_data', 'recur_interval', 'recur_exception', 'recur_rdates');
// only compare certain fields, taking account unset, null or '' values
$event = array_intersect_key(array_diff($_event, [null, ''])+array('recur_exception'=>array()), array_flip($keys_to_check));
@ -2560,9 +2576,17 @@ class calendar_uiforms extends calendar_ui
}
}
$ret = (bool)array_diff_assoc($event, $old);
//error_log(__METHOD__."() returning ".array2string($ret)." diff=".array2string(array_udiff_assoc($event, $old, function($a, $b) { return (int)($a != $b); })));
return $ret;
$changes = array_diff_assoc($event, $old);
if (!($changes['recure'] = array_values(array_filter(array_keys($changes), static function($name)
{
return substr($name, 0, 6) === 'recur_';
}))))
{
unset($changes['recure']);
}
return $changes;
}
/**

View File

@ -373,6 +373,7 @@ max. number of entries to show (leave empty for no restriction) calendar de Max.
maximum available quantity of %1 exceeded! calendar de Maximale Anzahl von %1 erreicht!
meeting canceled calendar de Termin abgesagt
meeting request calendar de Terminanfrage
meeting request additional recurrence(s) calendar de Terminanfrage zusätzliche Wiederholung(en)
meetingrequest to all participants calendar de Terminanforderung an alle Teilnehmer
merge document... calendar de Dokument einfügen ...
midnight calendar de Mitternacht
@ -601,6 +602,7 @@ the document can contain placeholder like {{%3}}, to be replaced with the data (
the exceptions are deleted together with the series. calendar de Die Ausnahmen werden mit der Terminserie gelöscht
the extern organizer changed the event! calendar de Der externe Organisator hat den Termin geändert!
the following document-types are supported: calendar de Im Moment werden die folgenden Dokumenttypen unterstützt:
the organizer changed the event! calendar de Der Organisator hat den Termin geändert!
the original series will be terminated today and a new series be created. calendar de Der bestehende Serientermin wird heute beendet und eine neue Terminserie erstellt.
the resource you selected is already overbooked: calendar de Die Ressource, die Sie buchen möchten, ist bereits überbucht
the sender "%1" is not the extern organizer "%2", proceed with caution! calendar de Der Absender "%1" ist nicht der externe Organisator "%2", seien Sie vorsichtig!
@ -613,6 +615,7 @@ this entry is opened by user: calendar de Dieser Eintrag ist von einem anderen B
this event is part of a series calendar de Dieser Termin ist Teil einer Serie
this group that is preselected when you enter the planner. you can change it in the planner anytime you want. calendar de Diese Gruppe wird als Vorauswahl ausgewählt wenn Sie den Planer öffnen. Sie können die Gruppe jederzeit wechseln wenn Sie möchten.
this is a recurring event. do you want to delete just this recurrence or the whole series? calendar de Das ist ein wiederholender Termin. Wollen Sie nur diese Wiederholung oder die ganze Serie löschen?
this is a single recurrence of a recurring event. calendar de Das ist eine einzelne Wiederholung einer Terminserie.
this mail cancels a meeting calendar de Diese Nachricht sagt einen Termin ab
this mail contains a meeting request calendar de Diese Nachricht enthält eine Terminanfrage
this mail contains a reply to a meeting request calendar de Diese Nachricht enthält eine Antwort auf eine Terminanfrage

View File

@ -373,6 +373,7 @@ max. number of entries to show (leave empty for no restriction) calendar en Max.
maximum available quantity of %1 exceeded! calendar en Maximum available quantity of %1 exceeded!
meeting canceled calendar en Meeting canceled
meeting request calendar en Meeting request
meeting request additional recurrence(s) calendar en Meeting request additional recurrence(s)
meetingrequest to all participants calendar en Meetingrequest to all participants
merge document... calendar en Merge document ...
midnight calendar en Midnight
@ -601,6 +602,7 @@ the document can contain placeholder like {{%3}}, to be replaced with the data (
the exceptions are deleted together with the series. calendar en The exceptions are deleted together with the series.
the extern organizer changed the event! calendar en The extern organizer changed the event!
the following document-types are supported: calendar en The following document-types are supported:
the organizer changed the event! calendar en The organizer changed the event!
the original series will be terminated today and a new series be created. calendar en The original series will be terminated today and a new series be created.
the resource you selected is already overbooked: calendar en The resource you selected is already over-booked:
the sender "%1" is not the extern organizer "%2", proceed with caution! calendar en The sender "%1" is NOT the extern organizer "%2", proceed with caution!
@ -613,6 +615,7 @@ this entry is opened by user: calendar en This entry was opened within the confi
this event is part of a series calendar en This event is part of a series
this group that is preselected when you enter the planner. you can change it in the planner anytime you want. calendar en This group that is preselected when you enter the planner. You can change it in the planner at any time.
this is a recurring event. do you want to delete just this recurrence or the whole series? calendar en This is a recurring event. Do you want to delete just this recurrence or the whole series?
this is a single recurrence of a recurring event. calendar en This is a single recurrence of a recurring event.
this mail cancels a meeting calendar en This mail cancels a meeting
this mail contains a meeting request calendar en This mail contains a meeting request
this mail contains a reply to a meeting request calendar en This mail contains a reply to a meeting request

View File

@ -27,6 +27,7 @@
<row disabled="!@ics_method=request">
<et2-vbox>
<et2-description value="This mail contains a meeting request" class="meetingRequestMessage"></et2-description>
<et2-textbox id="comment" placeholder="Comment" statustext="Add a comment to send to organizer with your response"></et2-textbox>
<et2-hbox class="buttonRow">
<et2-button label="Apply" id="button[apply]" hideOnReadonly="true"></et2-button>
<et2-button label="Accept" id="button[accept]" image="calendar/accepted"></et2-button>
@ -35,6 +36,7 @@
<et2-button statustext="Edit event in calendar" label="Edit" id="button[edit]" image="edit" hideOnReadonly="true"
onclick="window.open(egw::link('/index.php','menuaction=calendar.calendar_uiforms.edit&amp;cal_id=$cont[id]'),'_blank','dependent=yes,width=750,height=410,scrollbars=yes,status=yes'); return false;" noSubmit="true"></et2-button>
</et2-hbox>
<et2-description id="meeting-request-status" class="meetingRequestError" span="all"></et2-description>
</et2-vbox>
</row>
<row disabled="!@ics_method=reply">
@ -43,6 +45,7 @@
<et2-hbox class="buttonRow">
<et2-button label="Apply" id="button[apply]"></et2-button>
</et2-hbox>
<et2-description id="meeting-request-status" class="meetingRequestError" span="all"></et2-description>
</et2-vbox>
</row>
<row disabled="!@ics_method=cancel">
@ -73,12 +76,12 @@
</row>
<row class="row">
<et2-description value="Title"></et2-description>
<et2-description id="title" noLang="1"></et2-description>
<et2-description id="title" noLang="1" class="@changed[title]"></et2-description>
</row>
<row class="row">
<et2-description value="Location"></et2-description>
<et2-hbox>
<et2-description id="location" noLang="1"></et2-description>
<et2-description id="location" noLang="1" class="@changed[location]"></et2-description>
<et2-description class="et2_link" value="Videoconference" disabled="!@##videoconference" onclick="app.calendar.joinVideoConference(widget.getArrayMgr('content').getEntry('##videoconference'), widget.getArrayMgr('content').data);"></et2-description>
<et2-image src="videoconference" disabled="!@##videoconference"></et2-image>
</et2-hbox>
@ -86,25 +89,33 @@
<row class="row">
<et2-description value="Date"></et2-description>
<et2-hbox class="dates">
<et2-date-time id="start" readonly="true"></et2-date-time>
<et2-date-time label="-" id="end" readonly="true"></et2-date-time>
<et2-date-time id="start" readonly="true" class="@changed[start]"></et2-date-time>
<et2-date-time label="-" id="end" readonly="true" class="@changed[end]"></et2-date-time>
</et2-hbox>
</row>
<row class="row" disabled="!@recure">
<row class="row" disabled="!@recurrence">
<et2-description></et2-description>
<et2-description class="meetingRequestError" value="This is a single recurrence of a recurring event."></et2-description>
</row>
<row class="row" valign="top" disabled="!@recure">
<et2-description value="Recurrence"></et2-description>
<et2-description id="recure" noLang="1"></et2-description>
<et2-description id="recure" noLang="1" class="@changed[recure]"></et2-description>
</row>
<row class="row">
<et2-description value="Organizer"></et2-description>
<et2-url-email id="organizer" readonly="true"></et2-url-email>
</row>
<row class="row" valign="top">
<row class="row" valign="top" disabled="!@description">
<et2-description value="Description"></et2-description>
<et2-description id="description" noLang="ture" activateLinks="true"></et2-description>
<et2-description id="description" noLang="true" activateLinks="true" class="@changed[description]"></et2-description>
</row>
<row class="row" valign="top">
<et2-description value="Participants"></et2-description>
<et2-description id="all_participants"></et2-description>
<et2-description id="all_participants" class="@changed[participants]"></et2-description>
</row>
<row class="row" valign="top" disabled="!@comment">
<et2-description value="Comment"></et2-description>
<et2-description id="comment"></et2-description>
</row>
</rows>
</grid>
@ -118,9 +129,11 @@
}
.meetingRequestMessage {
font-size: 150%;
margin-bottom: 10px;
}
table.meetingRequest {
margin-top: 10px;
border: 2px solid black;
}
.meetingRequest tr.th {
@ -150,14 +163,10 @@
.meetingWarning tr.row td {
font-size: 120% !important;
}
.meetingRequestError {
.meetingRequestError, .meetingRequestChanged {
font-style: italic;
color: red;
}
.buttonRow {
padding-top: 10px;
padding-bottom: 10px;
}
</styles>
</template>
</overlay>