Branch 'dev/task-attendees' - 5 commits - plugins/libcalendaring plugins/libkolab plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Thu Jul 31 12:49:33 CEST 2014


 dev/null                                                 |binary
 plugins/libcalendaring/lib/libcalendaring_itip.php       |   15 -
 plugins/libcalendaring/localization/en_US.inc            |    8 
 plugins/libkolab/lib/kolab_format_event.php              |    2 
 plugins/libkolab/lib/kolab_format_task.php               |    2 
 plugins/libkolab/lib/kolab_format_xcal.php               |    5 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |    8 
 plugins/tasklist/localization/en_US.inc                  |   32 +-
 plugins/tasklist/skins/larry/images/attendee-status.png  |binary
 plugins/tasklist/skins/larry/images/badge_cancelled.png  |binary
 plugins/tasklist/skins/larry/tasklist.css                |   53 +++-
 plugins/tasklist/tasklist.js                             |  182 +++++++++++----
 plugins/tasklist/tasklist.php                            |   72 +++++
 plugins/tasklist/tasklist_base.js                        |    2 
 14 files changed, 304 insertions(+), 77 deletions(-)

New commits:
commit f5e93184e3ef08d2e4f9d57f3ade9a41390fe610
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 12:49:27 2014 +0200

    Fix iTip replies to organizer (don't increment sequence if I'm not the owner)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index ef8f31d..bcc5c06 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -902,9 +902,15 @@ class tasklist_kolab_driver extends tasklist_driver
             unset($object['attachments']);
         }
 
-        $object['_owner'] = $identity['email'];
+        // allow sequence increments if I'm the organizer
+        if ($this->plugin->is_organizer($object)) {
+            unset($object['sequence']);
+        }
+        else if (isset($old['sequence'])) {
+            $object['sequence'] = $old['sequence'];
+        }
 
-        unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['sequence']);
+        unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
         return $object;
     }
 
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 3953977..96b5e82 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -949,10 +949,11 @@ a.morelink:hover {
 	display: block;
 	color: #333;
 	font-weight: bold;
-	padding: 8px 4px 3px 30px;
+	padding: 3px 4px 3px 30px;
 	text-shadow: 0px 1px 1px #fff;
 	text-decoration: none;
 	white-space: nowrap;
+	line-height: 20px;
 }
 
 #taskedit-attachments ul li a.file {
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 9da7a88..a0ff22f 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -992,16 +992,14 @@ function rcube_tasklist_ui(settings)
           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');
-            }
+        if (has_attendees(rec) && 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
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index ae76f99..728c4a3 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1097,6 +1097,16 @@ class tasklist extends rcube_plugin
         return false;
     }
 
+    /**
+     * Determine whether the current user is the organizer of the given task
+     */
+    public function is_organizer($task)
+    {
+        $emails = $this->lib->get_user_emails();
+        return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
+    }
+
+
     /*******  UI functions  ********/
 
     /**


commit f8b6706074b394244b52d479e90dacd99f6c6cce
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 12:43:55 2014 +0200

    Fix 'create task from mail' function

diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 9ac70bc..ae76f99 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1128,11 +1128,17 @@ class tasklist extends rcube_plugin
 
         $texts['tasklist.newtask'] = $this->gettext('createfrommail');
 
+        // collect env variables
+        $env = array(
+            'tasklists' => array(),
+            'tasklist_settings' => $this->ui->load_settings(),
+        );
+
         $this->ui->init_templates();
         echo $this->api->output->parse('tasklist.taskedit', false, false);
         echo html::tag('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => $this->url($this->local_skin_path() . '/tagedit.css'), 'nl' => true));
         echo html::tag('script', array('type' => 'text/javascript'),
-            "rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
+            "rcmail.set_env(" . json_encode($env) . ");\n".
             "rcmail.add_label(" . json_encode($texts) . ");\n"
         );
         exit;
diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js
index 81e27f0..5448418 100644
--- a/plugins/tasklist/tasklist_base.js
+++ b/plugins/tasklist/tasklist_base.js
@@ -57,7 +57,7 @@ function rcube_tasklist(settings)
                     // rcmail.gui_object('attachmentlist', 'attachmentlist');
 
                     ui_loaded = true;
-                    me.ui = new rcube_tasklist_ui(settings);
+                    me.ui = new rcube_tasklist_ui($.extend(rcmail.env.tasklist_settings, settings));
                     create_from_mail(uid);  // start over
                 });
                 return;


commit 0d8b6912ae2864113a520b5f81a0d63447ecf8ff
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 12:22:07 2014 +0200

    Fix auto-incrementing the sequence value of xcal objects

diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 596d0da..c233f44 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -26,7 +26,7 @@ class kolab_format_event extends kolab_format_xcal
 {
     public $CTYPEv2 = 'application/x-vnd.kolab.event';
 
-    public static $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+    public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
 
     protected $objclass = 'Event';
     protected $read_func = 'readEvent';
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index ee0ca6a..52744d4 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -26,7 +26,7 @@ class kolab_format_task extends kolab_format_xcal
 {
     public $CTYPEv2 = 'application/x-vnd.kolab.task';
 
-    public static $scheduling_properties = array('start', 'due', 'summary', 'status');
+    public $scheduling_properties = array('start', 'due', 'summary', 'status');
 
     protected $objclass = 'Todo';
     protected $read_func = 'readTodo';
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 6624b02..7d077b7 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -29,7 +29,8 @@ abstract class kolab_format_xcal extends kolab_format
     public $CTYPE = 'application/calendar+xml';
 
     public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
-    public static $scheduling_properties = array('start', 'end', 'location');
+
+    public $scheduling_properties = array('start', 'end', 'location');
 
     protected $sensitivity_map = array(
         'public'       => kolabformat::ClassPublic,
@@ -315,7 +316,7 @@ abstract class kolab_format_xcal extends kolab_format
                 // increment sequence when updating properties relevant for scheduling.
                 // RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
                 // TODO: make the list of properties considered 'significant' for scheduling configurable
-                foreach (self::$scheduling_properties as $prop) {
+                foreach ($this->scheduling_properties as $prop) {
                     $a = $old[$prop];
                     $b = $object[$prop];
                     if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 6884083..ef8f31d 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -904,7 +904,7 @@ class tasklist_kolab_driver extends tasklist_driver
 
         $object['_owner'] = $identity['email'];
 
-        unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
+        unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['sequence']);
         return $object;
     }
 


commit 457195102e070c098899da7972b2fe6a25f572aa
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 11:40:39 2014 +0200

    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

diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index b37faa8..a7ec1ea 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 59b4493..5343e60 100644
Binary files a/plugins/tasklist/skins/larry/images/attendee-status.png and b/plugins/tasklist/skins/larry/images/attendee-status.png differ
diff --git a/plugins/tasklist/skins/larry/images/badge_cancelled.png b/plugins/tasklist/skins/larry/images/badge_cancelled.png
new file mode 100644
index 0000000..b89029e
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/badge_cancelled.png differ
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 8fa486a..0000000
Binary files a/plugins/tasklist/skins/larry/images/ical-attachment.png and /dev/null differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 1d891ed..3953977 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -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 **/
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 1b10773..9da7a88 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -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 });
@@ -982,6 +984,84 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
+     * 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
      */
     function unlock_saving()
@@ -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" /> ' +
+                    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" /> ' +
+                    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(){
@@ -2103,34 +2231,6 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
-     * 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
      */
     function match_filter(rec, cache, recursive)
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 62ddea0..9ac70bc 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


commit e46cc9499efb8539be4a8701155c32fabbd81d28
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 11:36:18 2014 +0200

    Add support for task-specific participant status values

diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index ece0e48..93bdfee 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -60,6 +60,11 @@ class libcalendaring_itip
         $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
     }
 
+    public function set_rsvp_status($status)
+    {
+        $this->rsvp_status = $status;
+    }
+
     /**
      * Wrapper for rcube_plugin::gettext()
      * Checking for a label in different domains
@@ -246,7 +251,8 @@ class libcalendaring_itip
         // attach ics file for this event
         $ical = libcalendaring::get_ical();
         $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
-        $message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
+        $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
+        $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
 
         return $message;
     }
@@ -321,9 +327,10 @@ class libcalendaring_itip
           $listed = false;
           foreach ($existing['attendees'] as $attendee) {
             if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
-              if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED','DELEGATED'))) {
-                $html = html::div('rsvp-status ' . strtolower($status), $this->gettext(array(
-                    'name' => 'attendee'.strtolower($status),
+              $status_lc = strtolower($status);
+              if (in_array($status_lc, $this->rsvp_status)) {
+                $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
+                    'name' => 'attendee' . $status_lc,
                     'vars' => array(
                         'delegatedto' => Q($attendee['delegated-to'] ?: '?'),
                     )
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 7e8c717..f3a506b 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -86,6 +86,8 @@ $labels['itipobjectnotfound'] = 'The object referred by this message was not fou
 $labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
 $labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
 $labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
+$labels['itipsubjectin-process'] = '"$title" is in-process by $name';
+$labels['itipsubjectcompleted'] = '"$title" was completed by $name';
 $labels['itipsubjectcancel'] = 'Your participation in "$title" has been cancelled';
 
 $labels['itipnewattendee'] = 'This is a reply from a new participant';
@@ -99,18 +101,24 @@ $labels['youhaveaccepted'] = 'You have accepted this invitation';
 $labels['youhavetentative'] = 'You have tentatively accepted this invitation';
 $labels['youhavedeclined'] = 'You have declined this invitation';
 $labels['youhavedelegated'] = 'You have delegated this invitation';
+$labels['youhavein-process'] = 'You are working on this assignment';
+$labels['youhavecompleted'] = 'You have completed this assignment';
 $labels['youhaveneeds-action'] = 'Your response to this invitation is still pending';
 
 $labels['youhavepreviouslyaccepted'] = 'You have previously accepted this invitation';
 $labels['youhavepreviouslytentative'] = 'You have previously accepted this invitation tentatively';
 $labels['youhavepreviouslydeclined'] = 'You have previously declined this invitation';
 $labels['youhavepreviouslydelegated'] = 'You have previously delegated this invitation';
+$labels['youhavepreviouslyin-process'] = 'You have previously reported to work on this assignment';
+$labels['youhavepreviouslycompleted'] = 'You have previously completed this assignment';
 $labels['youhavepreviouslyneeds-action'] = 'Your response to this invitation is still pending';
 
 $labels['attendeeaccepted'] = 'Participant has accepted';
 $labels['attendeetentative'] = 'Participant has tentatively accepted';
 $labels['attendeedeclined'] = 'Participant has declined';
 $labels['attendeedelegated'] = 'Participant has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Participant is in-process';
+$labels['attendeecompleted'] = 'Participant has completed';
 $labels['notanattendee'] = 'You\'re not listed as an attendee of this object';
 $labels['outdatedinvitation'] = 'This invitation has been replaced by a newer version';
 





More information about the commits mailing list