From 457195102e070c098899da7972b2fe6a25f572aa Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Thu, 31 Jul 2014 11:40:39 +0200 Subject: [PATCH] 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 --- plugins/tasklist/localization/en_US.inc | 32 ++- .../skins/larry/images/attendee-status.png | Bin 2919 -> 3203 bytes .../skins/larry/images/badge_cancelled.png | Bin 0 -> 1707 bytes .../skins/larry/images/ical-attachment.png | Bin 620 -> 0 bytes plugins/tasklist/skins/larry/tasklist.css | 50 +++-- plugins/tasklist/tasklist.js | 184 ++++++++++++++---- plugins/tasklist/tasklist.php | 54 ++++- 7 files changed, 255 insertions(+), 65 deletions(-) create mode 100644 plugins/tasklist/skins/larry/images/badge_cancelled.png delete mode 100644 plugins/tasklist/skins/larry/images/ical-attachment.png diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc index b37faa88..a7ec1ea3 100644 --- a/plugins/tasklist/localization/en_US.inc +++ b/plugins/tasklist/localization/en_US.inc @@ -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.'; diff --git a/plugins/tasklist/skins/larry/images/attendee-status.png b/plugins/tasklist/skins/larry/images/attendee-status.png index 59b44930d94931a6c15e59976b9489e183d20944..5343e6016decfe8225a6dec951899638dce19e1b 100644 GIT binary patch literal 3203 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 B_G|zE literal 0 HcmV?d00001 diff --git a/plugins/tasklist/skins/larry/images/ical-attachment.png b/plugins/tasklist/skins/larry/images/ical-attachment.png deleted file mode 100644 index 8fa486ae2e2332e56a96d39ef537bb330ed27a17..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 620 zcmV-y0+aoTP)$oVLBuY(iBp)wWdCr z%-egvc{orC?SXs0@80h_XU@HIr633pLz1K`JZOBFeE&UCII1QI0rqtJ8BeFYFXlPp ze!emO{M#qFto=$09IgdT_i^DNqpEG!AKbclb>+<-+U+(jkExAmU4i5HaNGcPAfYpo zquL~|DbsnpUG1I{RGNa}0dpK6D-CgT@hh0~2S0W!?0&bfv&+Yxjf25&s_okY4BU_- zxGWNoMSOXuj^_0-SVIRz_7_i#Y;pwU(gb%l37dtcV@@bw4JqDR>fy}kQ8<=|{=S6^ z=d(BpF$dxj`PDuOPKbyTIRIpzFBX=UNznKGBzm4lF}-(pW`;QbFOe_Y=si@cRj8^; zd8JaJAy6)tQ7V-v7rn6ni;g6_T)8nj3)gjF^IxLbHn!*H$bq6L==b~NK;)tqX#uXr zVh6R`Hzz~Fq+BWgJB?k_M$g9 z#V&|6W(mWM1K9=!)r|00RJ 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 = $('
').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 = $('
'); + + // 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 += '
' + + '
'; + } + else if (has_attendees(rec) && is_organizer(rec)) { + html += '
' + + '
'; + } + buttons.push({ text: rcmail.gettext('cancel', 'tasklist'), click: function() { @@ -2087,11 +2215,11 @@ function rcube_tasklist_ui(settings) } }); - var $dialog = $('
').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 */ diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index 62ddea0a..9ac70bca 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -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