Complete iTip communication on task status changes: ask to notify the organizer on update or deletion + add icons for task-specific partstats and cancelled tasks

This commit is contained in:
Thomas Bruederli 2014-07-31 11:40:39 +02:00
parent e46cc9499e
commit 457195102e
7 changed files with 255 additions and 65 deletions

View file

@ -51,6 +51,7 @@ $labels['newtask'] = 'New Task';
$labels['edittask'] = 'Edit Task';
$labels['save'] = 'Save';
$labels['cancel'] = 'Cancel';
$labels['saveandnotify'] = 'Save and Notify';
$labels['addsubtask'] = 'Add subtask';
$labels['deletetask'] = 'Delete task';
$labels['deletethisonly'] = 'Delete this task only';
@ -88,6 +89,9 @@ $labels['deleteparenttasktconfirm'] = 'Do you really want to delete this task an
$labels['deletelistconfirm'] = 'Do you really want to delete this list with all its tasks?';
$labels['deletelistconfirmrecursive'] = 'Do you really want to delete this list with all its sub-lists and tasks?';
$labels['aclnorights'] = 'You do not have administrator rights on this task list.';
$labels['changetaskconfirm'] = 'Update task';
$labels['changeconfirmnotifications'] = 'Do you want to notify the attendees about the modification?';
$labels['partstatupdatenotification'] = 'Do you want to notify the organizer about the status change?';
// (hidden) titles and labels for accessibility annotations
$labels['quickaddinput'] = 'New task date and title';
@ -124,17 +128,27 @@ $labels['saveintasklist'] = 'save in ';
// invitation handling (overrides labels from libcalendaring)
$labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nDue: \$date";
$labels['itipmailbodyin-process'] = "\$sender has set the status of the following task to in-process:\n\n*\$title*\n\nDue: \$date";
$labels['itipmailbodycompleted'] = "\$sender has completed the following task:\n\n*\$title*\n\nDue: \$date";
$labels['itipdeclineevent'] = 'Do you want to decline your assignment to this task?';
$labels['attendeeaccepted'] = 'Assignee has accepted';
$labels['attendeetentative'] = 'Assignee has tentatively accepted';
$labels['attendeedeclined'] = 'Assignee has declined';
$labels['attendeedelegated'] = 'Assignee has delegated to $delegatedto';
$labels['attendeein-process'] = 'Assignee is in-process';
$labels['attendeecompleted'] = 'Assignee has completed';
$labels['itipdeclinetask'] = 'Decline your assignment to this task to the organizer';
$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
$labels['itipcomment'] = 'Invitation/notification comment';
$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants';
$labels['itipsendsuccess'] = 'Invitation sent to participants.';
$labels['errornotifying'] = 'Failed to send notifications to task participants';
$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to assignees';
$labels['itipsendsuccess'] = 'Invitation sent to assignees';
$labels['errornotifying'] = 'Failed to send notifications to task assignees';
$labels['removefromcalendar'] = 'Remove from my tasks';
$labels['andnmore'] = '$nr more...';
$labels['delegatedto'] = 'Delegated to: ';
@ -149,7 +163,7 @@ $labels['nowritetasklistfound'] = 'No tasklist found to save the task';
$labels['importedsuccessfully'] = 'The task was successfully added to \'$list\'';
$labels['updatedsuccessfully'] = 'The task was successfully updated in \'$list\'';
$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
$labels['itipresponseerror'] = 'Failed to send the response to this task invitation';
$labels['itipresponseerror'] = 'Failed to send the response to this task assignment';
$labels['itipinvalidrequest'] = 'This invitation is no longer valid';
$labels['sentresponseto'] = 'Successfully sent invitation response to $mailto';
$labels['sentresponseto'] = 'Successfully sent assignment response to $mailto';
$labels['successremoval'] = 'The task has been deleted successfully.';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 620 B

View file

@ -815,6 +815,10 @@ ul.toolbarmenu li span.delete {
color: #999;
}
#taskshow.status-cancelled {
background: url(images/badge_cancelled.png) top right no-repeat;
}
#task-parent-title {
position: relative;
top: -0.6em;
@ -1043,7 +1047,7 @@ label.block {
text-decoration: underline;
}
.task-attendees span.accepted {
.task-attendees span.completed {
background-position: right -20px;
}
@ -1059,6 +1063,14 @@ label.block {
background-position: right -180px;
}
.task-attendees span.in-process {
background-position: right -200px;
}
.task-attendees span.accepted {
background-position: right -220px;
}
.task-attendees span.organizer {
background-position: right 100px;
}
@ -1082,12 +1094,7 @@ label.block {
width: 20em;
}
.ui-dialog .task-update-confirm {
padding: 0 0.5em 0.5em 0.5em;
}
.task-dialog-message,
.task-update-confirm .message {
.task-dialog-message {
margin-top: 0.5em;
padding: 0.8em;
border: 1px solid #ffdf0e;
@ -1161,35 +1168,54 @@ div.tasklist-invitebox .rsvp-status.hint {
}
#event-partstat .changersvp,
.edit-attendees-table td.confirmstate span,
div.tasklist-invitebox .rsvp-status.declined,
div.tasklist-invitebox .rsvp-status.tentative,
div.tasklist-invitebox .rsvp-status.accepted,
div.tasklist-invitebox .rsvp-status.delegated,
div.tasklist-invitebox .rsvp-status.needs-action {
div.tasklist-invitebox .rsvp-status.in-process,
div.tasklist-invitebox .rsvp-status.completed,
div.tasklist-invitebox .rsvp-status.needs-action {
padding: 0 0 1px 22px;
background: url(images/attendee-status.png) 2px -20px no-repeat;
}
#event-partstat .changersvp.declined,
div.tasklist-invitebox .rsvp-status.declined {
div.tasklist-invitebox .rsvp-status.declined,
.edit-attendees-table td.confirmstate span.declined {
background-position: 2px -40px;
}
#event-partstat .changersvp.tentative,
div.tasklist-invitebox .rsvp-status.tentative {
div.tasklist-invitebox .rsvp-status.tentative,
.edit-attendees-table td.confirmstate span.tentative {
background-position: 2px -60px;
}
#event-partstat .changersvp.delegated,
div.tasklist-invitebox .rsvp-status.delegated {
div.tasklist-invitebox .rsvp-status.delegated,
.edit-attendees-table td.confirmstate span.delegated {
background-position: 2px -180px;
}
#event-partstat .changersvp.needs-action,
div.tasklist-invitebox .rsvp-status.needs-action {
div.tasklist-invitebox .rsvp-status.needs-action,
.edit-attendees-table td.confirmstate span.needs-action {
background-position: 2px 0;
}
#event-partstat .changersvp.in-process,
div.tasklist-invitebox .rsvp-status.in-process,
.edit-attendees-table td.confirmstate span.in-process {
background-position: 2px -200px;
}
#event-partstat .changersvp.accepted,
div.tasklist-invitebox .rsvp-status.accepted,
.edit-attendees-table td.confirmstate span.accepted {
background-position: 2px -220px;
}
/** Special hacks for IE7 **/
/** They need to be in this file to also affect the task-create dialog embedded in mail view **/

View file

@ -353,9 +353,8 @@ function rcube_tasklist_ui(settings)
if (rcmail.busy)
return false;
rec.status = e.target.checked ? 'COMPLETED' : (rec.complete == 1 ? 'NEEDS-ACTION' : '');
li.toggleClass('complete');
save_task(rec, 'edit');
save_task_confirm(rec, 'edit', { _status_before:rec.status + '', status:e.target.checked ? 'COMPLETED' : (rec.complete > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION') });
item.toggleClass('complete');
return true;
case 'flagged':
@ -363,7 +362,7 @@ function rcube_tasklist_ui(settings)
return false;
rec.flagged = rec.flagged ? 0 : 1;
li.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
item.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
save_task(rec, 'edit');
break;
@ -377,8 +376,7 @@ function rcube_tasklist_ui(settings)
input.datepicker($.extend({
onClose: function(dateText, inst) {
if (dateText != (rec.date || '')) {
rec.date = dateText;
save_task(rec, 'edit');
save_task_confirm(rec, 'edit', { date:dateText });
}
input.datepicker('destroy').remove();
link.html(dateText || rcmail.gettext('nodate','tasklist'));
@ -971,6 +969,10 @@ function rcube_tasklist_ui(settings)
*/
function save_task(rec, action)
{
// show confirmation dialog when status of an assigned task has changed
if (rec._status_before !== undefined && is_attendee(rec))
return save_task_confirm(rec, action);
if (!rcmail.busy) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask });
@ -981,6 +983,84 @@ function rcube_tasklist_ui(settings)
return false;
}
/**
* Display confirm dialog when modifying/deleting a task record
*/
var save_task_confirm = function(rec, action, updates)
{
var data = $.extend({}, rec, updates || {}),
notify = false, partstat = false, html = '';
// task has attendees, ask whether to notify them
if (has_attendees(rec)) {
if (is_organizer(rec)) {
notify = true;
html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
}
// ask whether to change my partstat and notify organizer
else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
partstat = true;
html = rcmail.gettext('partstatupdatenotification', 'tasklist');
}
}
// remove to avoid endless recursion
delete data._status_before;
// show dialog
if (html) {
var $dialog = $('<div>').html(html);
var buttons = [];
buttons.push({
text: rcmail.gettext('saveandnotify', 'tasklist'),
click: function() {
if (notify) data._notify = 1;
if (partstat) data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status;
save_task(data, action);
$(this).dialog('close');
}
});
buttons.push({
text: rcmail.gettext('save', 'tasklist'),
click: function() {
save_task(data, action);
$(this).dialog('close');
}
});
buttons.push({
text: rcmail.gettext('cancel', 'tasklist'),
click: function() {
$(this).dialog('close');
if (updates)
render_task(rec, rec.id); // restore previous state
}
});
$dialog.dialog({
modal: true,
width: 460,
closeOnEscapeType: false,
dialogClass: 'warning no-close',
title: rcmail.gettext('changetaskconfirm', 'tasklist'),
buttons: buttons,
open: function() {
setTimeout(function(){
$dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
}, 5);
},
close: function(){
$dialog.dialog('destroy').remove();
}
}).addClass('task-update-confirm').show();
return true;
}
// do update
return save_task(data, action);
}
/**
* Remove saving lock and free the UI for new input
*/
@ -1488,6 +1568,12 @@ function rcube_tasklist_ui(settings)
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
// remove status-* classes
$dialog.removeClass(function(i, oldclass) {
var oldies = String(oldclass).split(' ');
return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' ');
});
if (!(rec = listdata[id]) || clear_popups({}))
return;
@ -1528,6 +1614,10 @@ function rcube_tasklist_ui(settings)
});
}
if (rec.status) {
$dialog.addClass('status-' + String(rec.status).toLowerCase());
}
if (rec.recurrence && rec.recurrence_text) {
$('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
}
@ -1817,6 +1907,7 @@ function rcube_tasklist_ui(settings)
var buttons = {};
buttons[rcmail.gettext('save', 'tasklist')] = function() {
var data = me.selected_task;
data._status_before = me.selected_task.status + '';
// copy form field contents into task object to save
$.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus, list:tasklist }, function(key,input){
@ -1867,6 +1958,8 @@ function rcube_tasklist_ui(settings)
data.complete = complete.val() / 100;
if (isNaN(data.complete))
data.complete = null;
else if (data.complete == 1.0 && rec.status === '')
data.status = 'COMPLETED';
if (!data.list && list.id)
data.list = list.id;
@ -1879,11 +1972,6 @@ function rcube_tasklist_ui(settings)
delete data.organizer;
}
// don't submit attendees if only myself is added as organizer
if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && String(data.attendees[0].email).toLowerCase() == settings.identity.email) {
data.attendees = [];
}
// per-attendee notification suppression
var need_invitation = false;
if (allow_invitations) {
@ -2050,7 +2138,34 @@ function rcube_tasklist_ui(settings)
if (!rec || rec.readonly || rcmail.busy)
return false;
var html, buttons = [];
var html, buttons = [], $dialog = $('<div>');
// Subfunction to submit the delete command after confirm
var _delete_task = function(id, mode) {
var rec = listdata[id],
li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide(),
decline = $dialog.find('input.confirm-attendees-decline:checked').length,
notify = $dialog.find('input.confirm-attendees-notify:checked').length;
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list, _decline:decline, _notify:notify }, mode:mode, filter:filtermask });
// move childs to parent/root
if (mode != 1 && rec.children !== undefined) {
var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
if (!parent_node || !parent_node.length)
parent_node = rcmail.gui_objects.resultlist;
$.each(rec.children, function(i,cid) {
var child = listdata[cid];
child.parent_id = rec.parent_id;
resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
});
}
li.remove();
delete listdata[id];
}
if (rec.children && rec.children.length) {
html = rcmail.gettext('deleteparenttasktconfirm','tasklist');
@ -2080,6 +2195,19 @@ function rcube_tasklist_ui(settings)
});
}
if (is_attendee(rec)) {
html += '<div class="task-dialog-message">' +
'<label><input class="confirm-attendees-decline" type="checkbox" checked="checked" value="1" name="_decline" />&nbsp;' +
rcmail.gettext('itipdeclinetask', 'tasklist') +
'</label></div>';
}
else if (has_attendees(rec) && is_organizer(rec)) {
html += '<div class="task-dialog-message">' +
'<label><input class="confirm-attendees-notify" type="checkbox" checked="checked" value="1" name="_notify" />&nbsp;' +
rcmail.gettext('sendcancellation', 'tasklist') +
'</label></div>';
}
buttons.push({
text: rcmail.gettext('cancel', 'tasklist'),
click: function() {
@ -2087,11 +2215,11 @@ function rcube_tasklist_ui(settings)
}
});
var $dialog = $('<div>').html(html);
$dialog.html(html);
$dialog.dialog({
modal: true,
width: 520,
dialogClass: 'warning',
dialogClass: 'warning no-close',
title: rcmail.gettext('deletetask', 'tasklist'),
buttons: buttons,
close: function(){
@ -2102,34 +2230,6 @@ function rcube_tasklist_ui(settings)
return true;
}
/**
* Subfunction to submit the delete command after confirm
*/
function _delete_task(id, mode)
{
var rec = listdata[id],
li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list }, mode:mode, filter:filtermask });
// move childs to parent/root
if (mode != 1 && rec.children !== undefined) {
var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
if (!parent_node || !parent_node.length)
parent_node = rcmail.gui_objects.resultlist;
$.each(rec.children, function(i,cid) {
var child = listdata[cid];
child.parent_id = rec.parent_id;
resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
});
}
li.remove();
delete listdata[id];
}
/**
* Check if the given task matches the current filtermask and tag selection
*/

View file

@ -342,6 +342,24 @@ class tasklist extends rcube_plugin
$this->rc->output->show_message('tasklist.errornotifying', 'error');
}
}
else if ($success && $rec['_reportpartstat']) {
// get the full record after update
$task = $this->driver->get_task($rec);
// send iTip REPLY with the updated partstat
if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) {
$sender = $task['attendees'][$idx];
$status = strtolower($sender['status']);
$itip = $this->load_itip();
$itip->set_sender_email($sender['email']);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
@ -367,6 +385,7 @@ class tasklist extends rcube_plugin
require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
$this->itip = new libcalendaring_itip($this, 'tasklist');
$this->itip->set_rsvp_actions(array('accepted','declined'));
$this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
}
return $this->itip;
@ -517,6 +536,20 @@ class tasklist extends rcube_plugin
$rec['attachments'] = $attachments;
// convert invalid data
if (isset($rec['attendees']) && !is_array($rec['attendees']))
$rec['attendees'] = array();
// copy the task status to my attendee partstat
if (!empty($rec['_reportpartstat'])) {
if (($idx = $this->is_attendee($rec)) !== false) {
if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED'))
$rec['attendees'][$idx]['status'] = $rec['_reportpartstat'];
else
unset($rec['_reportpartstat']);
}
}
// set organizer from identity selector
if (isset($rec['_identity']) && ($identity = $this->rc->user->get_identity($rec['_identity']))) {
$rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']);
@ -1044,9 +1077,25 @@ class tasklist extends rcube_plugin
else if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit))
$mask |= self::FILTER_MASK_LATER;
// TODO: add mask for "assigned to me"
return $mask;
}
/**
* Determine whether the current user is an attendee of the given task
*/
public function is_attendee($task)
{
$emails = $this->lib->get_user_emails();
foreach ((array)$task['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
return $i;
}
}
return false;
}
/******* UI functions ********/
@ -1709,7 +1758,7 @@ class tasklist extends rcube_plugin
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
$metadata['rsvp'] = intval($metadata['rsvp']);
$metadata['after_action'] = $this->rc->config->get('tasklist_itip_after_action');
$metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
$this->rc->output->command('plugin.itip_message_processed', $metadata);
$error_msg = null;
@ -1725,7 +1774,7 @@ class tasklist extends rcube_plugin
$itip->set_sender_email($reply_sender);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
@ -1813,6 +1862,7 @@ class tasklist extends rcube_plugin
public function to_libcal($task)
{
$object = $task;
$object['_type'] = 'task';
$object['categories'] = (array)$task['tags'];
// convert to datetime objects