Branch 'dev/task-attendees' - 2 commits - plugins/libcalendaring plugins/tasklist

Aleksander Machniak machniak at kolabsys.com
Fri Jul 25 14:09:14 CEST 2014


 plugins/libcalendaring/lib/libcalendaring_itip.php       |    2 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |   16 
 plugins/tasklist/drivers/tasklist_driver.php             |    1 
 plugins/tasklist/localization/en_US.inc                  |    6 
 plugins/tasklist/skins/larry/images/attendee-status.png  |binary
 plugins/tasklist/skins/larry/images/sendinvitation.png   |binary
 plugins/tasklist/skins/larry/tasklist.css                |   59 ++
 plugins/tasklist/skins/larry/templates/mainview.html     |   13 
 plugins/tasklist/skins/larry/templates/taskedit.html     |    2 
 plugins/tasklist/tasklist.js                             |  336 +++++++++------
 plugins/tasklist/tasklist.php                            |  140 ++++++
 plugins/tasklist/tasklist_ui.php                         |   24 -
 12 files changed, 472 insertions(+), 127 deletions(-)

New commits:
commit f2c6a3851d85a31d43057b5179c8b3216c23cb59
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Fri Jul 25 14:08:41 2014 +0200

    More changes for Assignments tab (#1165)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 1999a8e..624bdd5 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -25,16 +25,17 @@
 class tasklist_kolab_driver extends tasklist_driver
 {
     // features supported by the backend
-    public $alarms = false;
+    public $alarms      = false;
     public $attachments = true;
-    public $undelete = false; // task undelete action
+    public $attendees   = true;
+    public $undelete    = false; // task undelete action
     public $alarm_types = array('DISPLAY','AUDIO');
 
     private $rc;
     private $plugin;
     private $lists;
     private $folders = array();
-    private $tasks = array();
+    private $tasks   = array();
 
 
     /**
@@ -772,6 +773,7 @@ class tasklist_kolab_driver extends tasklist_driver
             'status' => $record['status'],
             'parent_id' => $record['parent_id'],
             'recurrence' => $record['recurrence'],
+            'attendees' => $record['attendees'],
         );
 
         // convert from DateTime to internal date format
@@ -898,6 +900,14 @@ class tasklist_kolab_driver extends tasklist_driver
             unset($object['attachments']);
         }
 
+        // set current user as ORGANIZER
+        $identity = $this->rc->user->get_identity();
+        if (empty($object['attendees']) && $identity['email']) {
+            $object['attendees'] = array(array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']));
+        }
+
+        $object['_owner'] = $identity['email'];
+
         unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
         return $object;
     }
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index eb7663e..dd2e415 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -72,6 +72,7 @@ abstract class tasklist_driver
     // features supported by the backend
     public $alarms = false;
     public $attachments = false;
+    public $attendees = false;
     public $undelete = false; // task undelete action
     public $sortable = false;
     public $alarm_types = array('DISPLAY');
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 7920c94..0b71377 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -132,3 +132,9 @@ $labels['itipdeclineevent'] = 'Do you want to decline your assignment to this ta
 $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['andnmore'] = '$nr more...';
+$labels['delegatedto'] = 'Delegated to: ';
+$labels['delegatedfrom'] = 'Delegated from: ';
diff --git a/plugins/tasklist/skins/larry/images/attendee-status.png b/plugins/tasklist/skins/larry/images/attendee-status.png
new file mode 100644
index 0000000..59b4493
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/attendee-status.png differ
diff --git a/plugins/tasklist/skins/larry/images/sendinvitation.png b/plugins/tasklist/skins/larry/images/sendinvitation.png
new file mode 100644
index 0000000..ecdaa09
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/sendinvitation.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 1e847b3..7bdfe57 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -1013,6 +1013,49 @@ label.block {
 	outline: none;
 }
 
+.task-attendees span.attendee {
+	padding-right: 18px;
+	margin-right: 0.5em;
+	background: url(images/attendee-status.png) right 0 no-repeat;
+}
+
+.task-attendees span.attendee a.mailtolink {
+	text-decoration: none;
+	white-space: nowrap;
+	outline: none;
+}
+
+.task-attendees span.attendee a.mailtolink:hover {
+	text-decoration: underline;
+}
+
+.task-attendees span.accepted {
+	background-position: right -20px;
+}
+
+.task-attendees span.declined {
+	background-position: right -40px;
+}
+
+.task-attendees span.tentative {
+	background-position: right -60px;
+}
+
+.task-attendees span.delegated {
+	background-position: right -180px;
+}
+
+.task-attendees span.organizer {
+	background-position: right -80px;
+}
+
+#all-task-attendees span.attendee {
+	display: block;
+	margin-bottom: 0.4em;
+	padding-bottom: 0.3em;
+	border-bottom: 1px solid #ddd;
+}
+
 .tasklistview .uidialog .tabbed {
 	min-width: 600px;
 }
@@ -1025,6 +1068,22 @@ label.block {
 	width: 20em;
 }
 
+.ui-dialog .task-update-confirm {
+	padding: 0 0.5em 0.5em 0.5em;
+}
+
+.task-dialog-message,
+.task-update-confirm .message {
+	margin-top: 0.5em;
+	padding: 0.8em;
+	border: 1px solid #ffdf0e;
+	background-color: #fef893;
+}
+
+.task-dialog-message .message,
+.task-update-confirm .message {
+	margin-bottom: 0.5em;
+}
 
 /** 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/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index 3003067..1881c76 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -156,6 +156,19 @@
 		<label><roundcube:label name="tasklist.alarms" /></label>
 		<span class="task-text"></span>
 	</div>
+	<div class="form-section task-attendees" id="task-attendees">
+		<h5 class="label"><roundcube:label name="tasklist.tabassignments" /></h5>
+		<div class="task-text"></div>
+	</div>
+<!--
+	<div class="form-section" id="task-partstat">
+		<label><roundcube:label name="tasklist.mystatus" /></label>
+		<span class="changersvp" role="button" tabindex="0" title="<roundcube:label name='tasklist.changepartstat' />">
+			<span class="task-text"></span>
+			<a class="iconbutton edit"><roundcube:label name='tasklist.changepartstat' /></a>
+		</span>
+	</div>
+-->
 	<div id="task-list" class="form-section">
 		<label><roundcube:label name="tasklist.list" /></label>
 		<span class="task-text"></span>
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 599974e..554028b 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -98,5 +98,5 @@
 			<roundcube:object name="plugin.filedroparea" id="taskedit-tab-2" />
 		</div>
 	</form>
-	<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="taskedit-dialog-message" style="display:none" />
+	<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="task-dialog-message" style="display:none" />
 </div>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index b0c95ba..03aed93 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -572,6 +572,19 @@ function rcube_tasklist_ui(settings)
           input.val('');
         }
       });
+
+      // handle change of "send invitations" checkbox
+      $('#edit-attendees-invite').change(function() {
+        $('#edit-attendees-donotify,input.edit-attendee-reply').prop('checked', this.checked);
+        // hide/show comment field
+        $('.attendees-commentbox')[this.checked ? 'show' : 'hide']();
+      });
+
+      // delegate change task to "send invitations" checkbox
+      $('#edit-attendees-donotify').change(function() {
+        $('#edit-attendees-invite').click();
+        return false;
+      });
     }
 
     /**
@@ -1338,11 +1351,13 @@ function rcube_tasklist_ui(settings)
     // check if the current user is an attendee of this task
     var is_attendee = function(task, role, email)
     {
-        var i, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
+        var i, attendee, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
 
         for (i=0; task.attendees && i < task.attendees.length; i++) {
-            if ((!role || task.attendees[i].role == role) && task.attendees[i].email && emails.indexOf(';'+task.attendees[i].email.toLowerCase()) >= 0)
-                return task.attendees[i];
+            attendee = task.attendees[i];
+            if ((!role || attendee.role == role) && attendee.email && emails.indexOf(';'+attendee.email.toLowerCase()) >= 0) {
+                return attendee;
+            }
         }
 
         return false;
@@ -1357,140 +1372,144 @@ function rcube_tasklist_ui(settings)
     // add the given list of participants
     var add_attendees = function(names, params)
     {
-      names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
-
-      // parse name/email pairs
-      var item, email, name, success = false;
-      for (var i=0; i < names.length; i++) {
-        email = name = '';
-        item = $.trim(names[i]);
-
-        if (!item.length) {
-          continue;
-        } // address in brackets without name (do nothing)
-        else if (item.match(/^<[^@]+@[^>]+>$/)) {
-          email = item.replace(/[<>]/g, '');
-        } // address without brackets and without name (add brackets)
-        else if (rcube_check_email(item)) {
-          email = item;
-        } // address with name
-        else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
-          email = RegExp.$1;
-          name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
-        }
-        if (email) {
-          add_attendee($.extend({ email:email, name:name }, params));
-          success = true;
-        }
-        else {
-          alert(rcmail.gettext('noemailwarning'));
+        names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
+
+        // parse name/email pairs
+        var i, item, email, name, success = false;
+        for (i=0; i < names.length; i++) {
+            email = name = '';
+            item = $.trim(names[i]);
+
+            if (!item.length) {
+                continue;
+            }
+            // address in brackets without name (do nothing)
+            else if (item.match(/^<[^@]+@[^>]+>$/)) {
+                email = item.replace(/[<>]/g, '');
+            }
+            // address without brackets and without name (add brackets)
+            else if (rcube_check_email(item)) {
+                email = item;
+            }
+            // address with name
+            else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
+                email = RegExp.$1;
+                name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
+            }
+
+            if (email) {
+                add_attendee($.extend({ email:email, name:name }, params));
+                success = true;
+            }
+            else {
+                alert(rcmail.gettext('noemailwarning'));
+            }
         }
-      }
 
-      return success;
+        return success;
     };
 
     // add the given attendee to the list
     var add_attendee = function(data, readonly)
     {
-      if (!me.selected_task)
-        return false;
+        if (!me.selected_task)
+            return false;
 
-      // check for dupes...
-      var exists = false;
-      $.each(task_attendees, function(i, v) { exists |= (v.email == data.email); });
-      if (exists)
-        return false;
+        // check for dupes...
+        var exists = false;
+        $.each(task_attendees, function(i, v) { exists |= (v.email == data.email); });
+        if (exists)
+            return false;
 
-      var list = me.selected_task && me.tasklists[me.selected_task.list] ? me.tasklists[me.selected_task.list] : me.tasklists[me.selected_list];
-
-      var dispname = Q(data.name || data.email);
-      if (data.email)
-        dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
-
-      // role selection
-      var organizer = data.role == 'ORGANIZER';
-      var opts = {};
-      if (organizer)
-        opts.ORGANIZER = rcmail.gettext('.roleorganizer');
-      opts['REQ-PARTICIPANT'] = rcmail.gettext('tasklist.rolerequired');
-      opts['OPT-PARTICIPANT'] = rcmail.gettext('tasklist.roleoptional');
-      opts['NON-PARTICIPANT'] = rcmail.gettext('tasklist.rolenonparticipant');
-
-      if (data.cutype != 'RESOURCE')
-        opts['CHAIR'] =  rcmail.gettext('tasklist.rolechair');
-
-      if (organizer && !readonly)
-          dispname = rcmail.env['identities-selector'];
-
-      var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','tasklist') + '">';
-      for (var r in opts)
-        select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
-      select += '</select>';
-
-      // availability
-      var avail = data.email ? 'loading' : 'unknown';
-
-      // delete icon
-      var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
-      var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
-      var tooltip = data.status || '';
-
-      // send invitation checkbox
-      var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('tasklist.sendinvitations')) + '" '
-        + (!data.noreply ? 'checked="checked" ' : '') + '/>';
-
-      if (data['delegated-to'])
-        tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
-      else if (data['delegated-from'])
-        tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
-
-      var html = '<td class="role">' + select + '</td>' +
-        '<td class="name">' + dispname + '</td>' +
-//        '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
-        '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
-        (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
-        '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
-
-      var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
-      var tr = $('<tr>')
-        .addClass(String(data.role).toLowerCase())
-        .html(html)
-        .appendTo(table);
-
-      tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
-      tr.find('a.mailtolink').click(task_attendee_click);
-      tr.find('input.edit-attendee-reply').click(function() {
-        var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
-        $('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
-      });
+//      var list = me.selected_task && me.tasklists[me.selected_task.list] ? me.tasklists[me.selected_task.list] : me.tasklists[me.selected_list];
+
+        var dispname = Q(data.name || data.email);
+        if (data.email)
+            dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+
+        // role selection
+        var opts = {}, organizer = data.role == 'ORGANIZER';
+        if (organizer)
+            opts.ORGANIZER = rcmail.gettext('tasklist.roleorganizer');
+        opts['REQ-PARTICIPANT'] = rcmail.gettext('tasklist.rolerequired');
+        opts['OPT-PARTICIPANT'] = rcmail.gettext('tasklist.roleoptional');
+        opts['NON-PARTICIPANT'] = rcmail.gettext('tasklist.rolenonparticipant');
+
+        if (data.cutype != 'RESOURCE')
+            opts['CHAIR'] =  rcmail.gettext('tasklist.rolechair');
+
+        if (organizer && !readonly)
+            dispname = rcmail.env['identities-selector'];
+
+        var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','tasklist') + '">';
+        for (var r in opts)
+            select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
+        select += '</select>';
+
+        // availability
+        var avail = data.email ? 'loading' : 'unknown';
+
+        // delete icon
+        var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
+        var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
+        var tooltip = data.status || '';
+
+        // send invitation checkbox
+        var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('tasklist.sendinvitations')) + '" '
+            + (!data.noreply ? 'checked="checked" ' : '') + '/>';
+
+        if (data['delegated-to'])
+            tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
+        else if (data['delegated-from'])
+            tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+
+        var html = '<td class="role">' + select + '</td>' +
+            '<td class="name">' + dispname + '</td>' +
+//            '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
+            '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
+            (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
+            '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
+
+        var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
+        var tr = $('<tr>')
+            .addClass(String(data.role).toLowerCase())
+            .html(html)
+            .appendTo(table);
+
+        tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
+        tr.find('a.mailtolink').click(task_attendee_click);
+        tr.find('input.edit-attendee-reply').click(function() {
+            var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
+            $('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
+        });
 
-      // select organizer identity
-      if (data.identity_id)
-        $('#edit-identities-list').val(data.identity_id);
+        // select organizer identity
+        if (data.identity_id)
+            $('#edit-identities-list').val(data.identity_id);
 
       // check free-busy status
 //      if (avail == 'loading') {
 //        check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_task);
 //      }
 
-      task_attendees.push(data);
-      return true;
+        task_attendees.push(data);
+        return true;
     };
 
     // event handler for clicks on an attendee link
     var task_attendee_click = function(e)
     {
-      var cutype = $(this).attr('data-cutype'),
-        mailto = this.href.substr(7);
+        var cutype = $(this).attr('data-cutype'),
+            mailto = this.href.substr(7);
 
-      if (rcmail.env.calendar_resources && cutype == 'RESOURCE') {
-        event_resources_dialog(mailto);
-      }
-      else {
-        rcmail.redirect(rcmail.url('mail/compose', { _to:mailto }));
-      }
-      return false;
+        if (rcmail.env.tasklist_resources && cutype == 'RESOURCE') {
+            task_resources_dialog(mailto);
+        }
+        else {
+            rcmail.redirect(rcmail.url('mail/compose', {_to: mailto}));
+        }
+
+        return false;
     };
 
     // remove an attendee from the list
@@ -1528,6 +1547,7 @@ function rcube_tasklist_ui(settings)
         $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
         $('#task-status')[(rec.status ? 'show' : 'hide')]().children('.task-text').html(rcmail.gettext('status-'+String(rec.status).toLowerCase(),'tasklist'));
         $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
+        $('#task-attendees').hide();
 
         var itags = get_inherited_tags(rec);
         var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty();
@@ -1565,6 +1585,90 @@ function rcube_tasklist_ui(settings)
             }
         }
 
+        // list task attendees
+        if (list.attendees && rec.attendees) {
+/*
+            // sort resources to the end
+            rec.attendees.sort(function(a,b) {
+                var j = a.cutype == 'RESOURCE' ? 1 : 0,
+                    k = b.cutype == 'RESOURCE' ? 1 : 0;
+                return (j - k);
+            });
+*/
+            var j, data, dispname, tooltip, organizer = false, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '';
+            for (j=0; j < rec.attendees.length; j++) {
+                data = rec.attendees[j];
+                dispname = Q(data.name || data.email);
+                tooltip = '';
+
+                if (data.email) {
+                    tooltip = data.email;
+                    dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+                    if (data.role == 'ORGANIZER')
+                        organizer = true;
+                    else if (settings.identity.emails.indexOf(';'+data.email) >= 0) {
+                        mystatus = data.status.toLowerCase();
+                        if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
+                            rsvp = mystatus;
+                    }
+                }
+
+                if (data['delegated-to'])
+                    tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
+                else if (data['delegated-from'])
+                    tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+
+                line = '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+
+                if (morelink)
+                    overflow += line;
+                else
+                    html += line;
+
+                // stop listing attendees
+                if (j == 7 && rec.attendees.length >= 7) {
+                    morelink = $('<a href="#more" class="morelink"></a>').html(rcmail.gettext('andnmore', 'tasklist').replace('$nr', rec.attendees.length - j - 1));
+                }
+            }
+
+            if (html && (rec.attendees.length > 1 || !organizer)) {
+                $('#task-attendees').show()
+                    .children('.task-text')
+                    .html(html)
+                    .find('a.mailtolink').click(task_attendee_click);
+
+                // display all attendees in a popup when clicking the "more" link
+                if (morelink) {
+                    $('#task-attendees .task-text').append(morelink);
+                    morelink.click(function(e) {
+                        rcmail.show_popup_dialog(
+                            '<div id="all-task-attendees" class="task-attendees">' + html + overflow + '</div>',
+                            rcmail.gettext('tabattendees', 'tasklist'),
+                            null,
+                            {width: 450, modal: false}
+                        );
+                        $('#all-task-attendees a.mailtolink').click(task_attendee_click);
+                        return false;
+                    });
+                }
+            }
+/*
+            if (mystatus && !rsvp) {
+                $('#task-partstat').show().children('.changersvp')
+                    .removeClass('accepted tentative declined delegated needs-action')
+                    .addClass(mystatus)
+                    .children('.task-text')
+                    .html(Q(rcmail.gettext('itip' + mystatus, 'libcalendaring')));
+            }
+
+            $('#task-rsvp')[(rsvp && !is_organizer(event) && rec.status != 'CANCELLED' ? 'show' : 'hide')]();
+            $('#task-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true);
+
+            $('#task-rsvp a.reply-comment-toggle').show();
+            $('#task-rsvp .itip-reply-comment textarea').hide().val('');
+*/
+        }
+
         // define dialog buttons
         var buttons = [];
         if (list.editable && !rec.readonly) {
@@ -1874,7 +1978,7 @@ function rcube_tasklist_ui(settings)
         // set dialog size according to content
         me.dialog_resize($dialog.get(0), $dialog.height(), 580);
 
-        if (tasklist.attendees)
+        if (list.attendees)
             window.setTimeout(load_attendees_tab, 1);
     }
 
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index d227f54..34b1ffb 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -55,6 +55,7 @@ class tasklist extends rcube_plugin
     public $home;  // declare public to be used in other classes
 
     private $collapsed_tasks = array();
+    private $itip;
 
 
     /**
@@ -64,7 +65,7 @@ class tasklist extends rcube_plugin
     {
         $this->require_plugin('libcalendaring');
 
-        $this->rc = rcube::get_instance();
+        $this->rc  = rcube::get_instance();
         $this->lib = libcalendaring::get_instance();
 
         $this->register_task('tasks', 'tasklist');
@@ -188,7 +189,7 @@ class tasklist extends rcube_plugin
     {
         $filter = intval(get_input_value('filter', RCUBE_INPUT_GPC));
         $action = get_input_value('action', RCUBE_INPUT_GPC);
-        $rec  = get_input_value('t', RCUBE_INPUT_POST, true);
+        $rec    = get_input_value('t', RCUBE_INPUT_POST, true);
         $oldrec = $rec;
         $success = $refresh = false;
 
@@ -318,8 +319,24 @@ class tasklist extends rcube_plugin
             $this->rc->output->show_message('successfullysaved', 'confirmation');
             $this->update_counts($oldrec, $refresh);
         }
-        else
+        else {
             $this->rc->output->show_message('tasklist.errorsaving', 'error');
+        }
+
+        // send out notifications
+        if ($success && $rec['_notify'] && ($rec['attendees'] || $oldrec['attendees'])) {
+            // make sure we have the complete record
+            $task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec);
+
+            // only notify if data really changed (TODO: do diff check on client already)
+            if (!$oldrec || $action == 'delete' || self::task_diff($event, $old)) {
+                $sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']);
+                if ($sent > 0)
+                    $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
+                else if ($sent < 0)
+                    $this->rc->output->show_message('tasklist.errornotifying', 'error');
+            }
+        }
 
         // unlock client
         $this->rc->output->command('plugin.unlock_saving');
@@ -337,6 +354,23 @@ class tasklist extends rcube_plugin
     }
 
     /**
+     * Load iTIP functions
+     */
+    private function load_itip()
+    {
+        if (!$this->itip) {
+            require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
+            $this->itip = new libcalendaring_itip($this, 'tasklist');
+
+//            if ($this->rc->config->get('kolab_invitation_tasklists')) {
+//                $this->itip->set_rsvp_actions(array('accepted','tentative','declined','needs-action'));
+//            }
+        }
+
+        return $this->itip;
+    }
+
+    /**
      * repares new/edited task properties before save
      */
     private function prepare_task($rec)
@@ -587,6 +621,106 @@ class tasklist extends rcube_plugin
     }
 
     /**
+     * Send out an invitation/notification to all task attendees
+     */
+    private function notify_attendees($task, $old, $action = 'edit', $comment = null)
+    {
+        if ($action == 'delete' || ($task['status'] == 'CANCELLED' && $old['status'] != $task['status'])) {
+            $task['cancelled'] = true;
+            $is_cancelled      = true;
+        }
+
+        $itip   = $this->load_itip();
+        $emails = $this->lib->get_user_emails();
+
+        // add comment to the iTip attachment
+        $task['comment'] = $comment;
+
+        // needed to generate VTODO instead of VEVENT entry
+        $task['_type'] = 'task';
+
+        // compose multipart message using PEAR:Mail_Mime
+        $method  = $action == 'delete' ? 'CANCEL' : 'REQUEST';
+        $message = $itip->compose_itip_message($task, $method);
+
+        // list existing attendees from the $old task
+        $old_attendees = array();
+        foreach ((array)$old['attendees'] as $attendee) {
+            $old_attendees[] = $attendee['email'];
+        }
+
+        // send to every attendee
+        $sent = 0; $current = array();
+        foreach ((array)$task['attendees'] as $attendee) {
+            $current[] = strtolower($attendee['email']);
+
+            // skip myself for obvious reasons
+            if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) {
+                continue;
+            }
+
+            // skip if notification is disabled for this attendee
+            if ($attendee['noreply']) {
+                continue;
+            }
+
+            // which template to use for mail text
+            $is_new   = !in_array($attendee['email'], $old_attendees);
+            $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
+            $subject  = $is_cancelled ? 'eventcancelsubject'  : ($is_new ? 'invitationsubject' : ($task['title'] ? 'eventupdatesubject' : 'eventupdatesubjectempty'));
+
+            // finally send the message
+            if ($itip->send_itip_message($task, $method, $attendee, $subject, $bodytext, $message))
+                $sent++;
+            else
+                $sent = -100;
+        }
+
+        // send CANCEL message to removed attendees
+        foreach ((array)$old['attendees'] as $attendee) {
+            if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
+                continue;
+            }
+
+            $vevent = $old;
+            $vevent['cancelled'] = $is_cancelled;
+            $vevent['attendees'] = array($attendee);
+            $vevent['comment']   = $comment;
+
+            if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody'))
+                $sent++;
+            else
+                $sent = -100;
+        }
+
+        return $sent;
+    }
+
+    /**
+     * Compare two task objects and return differing properties
+     *
+     * @param array Event A
+     * @param array Event B
+     * @return array List of differing task properties
+     */
+    public static function task_diff($a, $b)
+    {
+        $diff   = array();
+        $ignore = array('changed' => 1, 'attachments' => 1);
+
+        foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
+            if (!$ignore[$key] && $a[$key] != $b[$key])
+                $diff[] = $key;
+        }
+
+        // only compare number of attachments
+        if (count($a['attachments']) != count($b['attachments']))
+            $diff[] = 'attachments';
+
+        return $diff;
+    }
+
+    /**
      * Dispatcher for tasklist actions initiated by the client
      */
     public function tasklist_action()
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 4dd7b82..5fc0a20 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -56,6 +56,7 @@ class tasklist_ui
 
         // copy config to client
         $this->rc->output->set_env('tasklist_settings', $this->load_settings());
+        $this->rc->output->set_env('identities-selector', $this->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->plugin->gettext('roleorganizer'))));
 
         // initialize attendees autocompletion
         $this->rc->autocomplete_init();
@@ -92,6 +93,22 @@ class tasklist_ui
     }
 
     /**
+     * Render a HTML select box for user identity selection
+     */
+    function identity_select($attrib = array())
+    {
+        $attrib['name'] = 'identity';
+        $select         = new html_select($attrib);
+        $identities     = $this->rc->user->list_identities();
+
+        foreach ($identities as $ident) {
+            $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
+        }
+
+        return $select->show(null);
+    }
+
+    /**
     * Register handler methods for the template engine
     */
     public function init_templates()
@@ -200,10 +217,11 @@ class tasklist_ui
         // enrich list properties with settings from the driver
         if (!$prop['virtual']) {
             unset($prop['user_id']);
-            $prop['alarms'] = $this->plugin->driver->alarms;
-            $prop['undelete'] = $this->plugin->driver->undelete;
-            $prop['sortable'] = $this->plugin->driver->sortable;
+            $prop['alarms']      = $this->plugin->driver->alarms;
+            $prop['undelete']    = $this->plugin->driver->undelete;
+            $prop['sortable']    = $this->plugin->driver->sortable;
             $prop['attachments'] = $this->plugin->driver->attachments;
+            $prop['attendees']   = $this->plugin->driver->attendees;
             $jsenv[$id] = $prop;
         }
 


commit 6fad5ede559746d4697c083d047dedf26058f00a
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Fri Jul 25 14:01:13 2014 +0200

    Don't depend on get_ical() method existence

diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 074f87f..a6fca9c 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -236,7 +236,7 @@ class libcalendaring_itip
         $message->headers($headers);
 
         // attach ics file for this event
-        $ical = $this->plugin->get_ical();
+        $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);
 




More information about the commits mailing list