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

Thomas Brüderli bruederli at kolabsys.com
Wed Jul 30 17:42:56 CEST 2014


 plugins/libcalendaring/lib/libcalendaring_itip.php       |   20 +-
 plugins/libcalendaring/libcalendaring.php                |   18 +
 plugins/libcalendaring/libvcalendar.php                  |    2 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |   16 -
 plugins/tasklist/localization/en_US.inc                  |   35 +--
 plugins/tasklist/skins/larry/tasklist.css                |   23 +-
 plugins/tasklist/skins/larry/templates/mainview.html     |   12 -
 plugins/tasklist/skins/larry/templates/taskedit.html     |    4 
 plugins/tasklist/tasklist.js                             |  147 +++++----------
 plugins/tasklist/tasklist.php                            |  121 ++++++++++--
 plugins/tasklist/tasklist_ui.php                         |    7 
 11 files changed, 243 insertions(+), 162 deletions(-)

New commits:
commit b3c5acd66a088248c5634bdb14e5faa3e2011cf4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 30 17:40:53 2014 +0200

    - Fix task attendees and organizer setting and display
    - Make basic iTip exchange for task assignments work
    - Improve wording for task assignments

diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index a6fca9c..ece0e48 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -213,6 +213,14 @@ class libcalendaring_itip
                 $event['attendees'] = $reply_attendees;
             }
         }
+        // set RSVP=TRUE for every attendee if not set
+        else if ($method == 'REQUEST') {
+            foreach ($event['attendees'] as $i => $attendee) {
+                if (!isset($attendee['rsvp'])) {
+                    $event['attendees'][$i]['rsvp']= true;
+                }
+            }
+        }
 
         // compose multipart message using PEAR:Mail_Mime
         $message = new Mail_mime("\r\n");
@@ -532,15 +540,21 @@ class libcalendaring_itip
     }
 
     /**
-     * Render event details in a table
+     * Render event/task details in a table
      */
     function itip_object_details_table($event, $title)
     {
         $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
         $table->add('ititle', $title);
         $table->add('title', Q($event['title']));
-        $table->add('label', $this->plugin->gettext('date'), $this->domain);
-        $table->add('date', Q($this->lib->event_date_text($event)));
+        if ($event['start'] && $event['end']) {
+            $table->add('label', $this->plugin->gettext('date'), $this->domain);
+            $table->add('date', Q($this->lib->event_date_text($event)));
+        }
+        else if ($event['due'] && $event['_type'] == 'task') {
+            $table->add('label', $this->plugin->gettext('date'), $this->domain);
+            $table->add('date', Q($this->lib->event_date_text($event)));
+        }
         if ($event['location']) {
             $table->add('label', $this->plugin->gettext('location'), $this->domain);
             $table->add('location', Q($event['location']));
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 5a1a8b0..36fc287 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -247,11 +247,25 @@ class libcalendaring extends rcube_plugin
      */
     public function event_date_text($event, $tzinfo = false)
     {
-        $fromto = '';
+        $fromto = '--';
+
+        // handle task objects
+        if ($event['_type'] == 'task' && is_object($event['due'])) {
+            $date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
+            $fromto = $this->rc->format_date($event['due'], $date_format, false);
+
+            // add timezone information
+            if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) {
+                $fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
+            }
+
+            return $fromto;
+        }
 
         // abort if no valid event dates are given
-        if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime'))
+        if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
             return $fromto;
+        }
 
         $duration = $event['start']->diff($event['end'])->format('s');
 
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 855e074..a89cec2 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -567,7 +567,7 @@ class libvcalendar implements Iterator
         }
 
         // make organizer part of the attendees list for compatibility reasons
-        if (!empty($event['organizer']) && is_array($event['attendees'])) {
+        if (!empty($event['organizer']) && is_array($event['attendees']) && $event['_type'] == 'event') {
             array_unshift($event['attendees'], $event['organizer']);
         }
 
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 624bdd5..6884083 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -774,6 +774,8 @@ class tasklist_kolab_driver extends tasklist_driver
             'parent_id' => $record['parent_id'],
             'recurrence' => $record['recurrence'],
             'attendees' => $record['attendees'],
+            'organizer' => $record['organizer'],
+            'sequence' => $record['sequence'],
         );
 
         // convert from DateTime to internal date format
@@ -817,8 +819,8 @@ class tasklist_kolab_driver extends tasklist_driver
     }
 
     /**
-    * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
-    * (opposite of self::_to_rcube_event())
+     * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
+     * (opposite of self::_to_rcube_event())
      */
     private function _from_rcube_task($task, $old = array())
     {
@@ -826,14 +828,14 @@ class tasklist_kolab_driver extends tasklist_driver
         $object['categories'] = (array)$task['tags'];
 
         if (!empty($task['date'])) {
-            $object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone);
+            $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->plugin->timezone);
             if (empty($task['time']))
                 $object['due']->_dateonly = true;
             unset($object['date']);
         }
 
         if (!empty($task['startdate'])) {
-            $object['start'] = new DateTime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
+            $object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
             if (empty($task['starttime']))
                 $object['start']->_dateonly = true;
             unset($object['startdate']);
@@ -900,12 +902,6 @@ 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']);
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index f43fbb9..b37faa8 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -33,6 +33,7 @@ $labels['status-needs-action'] = 'Needs action';
 $labels['status-in-process'] = 'In process';
 $labels['status-completed'] = 'Completed';
 $labels['status-cancelled'] = 'Cancelled';
+$labels['assignedto'] = 'Assigned to';
 
 $labels['all'] = 'All';
 $labels['flagged'] = 'Flagged';
@@ -98,35 +99,35 @@ $labels['arialabeltaskselector'] = 'List mode';
 $labels['arialabeltasklisting'] = 'Tasks listing';
 
 // attendees
-$labels['attendee'] = 'Participant';
+$labels['attendee'] = 'Assignee';
 $labels['role'] = 'Role';
 $labels['availability'] = 'Avail.';
 $labels['confirmstate'] = 'Status';
-$labels['addattendee'] = 'Add participant';
+$labels['addattendee'] = 'Add assignee';
 $labels['roleorganizer'] = 'Organizer';
 $labels['rolerequired'] = 'Required';
 $labels['roleoptional'] = 'Optional';
 $labels['rolechair'] = 'Chair';
-$labels['rolenonparticipant'] = 'Absent';
+$labels['rolenonparticipant'] = 'Observer';
 $labels['sendinvitations'] = 'Send invitations';
-$labels['sendnotifications'] = 'Notify participants about modifications';
-$labels['sendcancellation'] = 'Notify participants about task cancellation';
-$labels['invitationsubject'] = 'You\'ve been invited to "$title"';
-$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the task details which you can import to your tasks application.";
-$labels['invitationattendlinks'] = "In case your email client doesn't support iTip requests you can use the following link to either accept or decline this invitation:\n\$url";
-$labels['eventupdatesubject'] = '"$title" has been updated';
-$labels['eventupdatesubjectempty'] = 'A task that concerns you has been updated';
-$labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated task details which you can import to your tasks application.";
-$labels['eventcancelsubject'] = '"$title" has been canceled';
-$labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details.";
+$labels['sendnotifications'] = 'Notify assignees about modifications';
+$labels['sendcancellation'] = 'Notify assignees about task cancellation';
+$labels['invitationsubject'] = 'You\'ve been assigned to "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nPlease find attached an iCalendar file with all the task details which you can import to your tasks application.";
+$labels['itipupdatesubject'] = '"$title" has been updated';
+$labels['itipupdatesubjectempty'] = 'A task that concerns you has been updated';
+$labels['itipupdatemailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nPlease find attached an iCalendar file with the updated task details which you can import to your tasks application.";
+$labels['itipcancelsubject'] = '"$title" has been canceled';
+$labels['itipcancelmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details.";
+$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\nWhen: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nWhen: \$date";
+$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['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nDue: \$date";
 
 $labels['itipdeclineevent'] = 'Do you want to decline your assignment to this task?';
 $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index bcdf29a..1d891ed 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -855,6 +855,14 @@ a.morelink:hover {
 	margin-top: 0.5em;
 }
 
+.edit-attendees-table tbody td {
+	padding: 4px 7px;
+}
+
+.edit-attendees-table tbody tr:last-child td {
+	border-bottom: 0;
+}
+
 .edit-attendees-table th.role,
 .edit-attendees-table td.role {
 	width: 9em;
@@ -864,18 +872,19 @@ a.morelink:hover {
 .edit-attendees-table td.availability,
 .edit-attendees-table th.confirmstate,
 .edit-attendees-table td.confirmstate {
-	width: 4em;
+	width: 6em;
 }
 
 .edit-attendees-table th.options,
 .edit-attendees-table td.options {
-	width: 16px;
+	width: 24px;
 	padding: 2px 4px;
+	text-align: right;
 }
 
 .edit-attendees-table th.sendmail,
 .edit-attendees-table td.sendmail {
-	width: 44px;
+	width: 48px;
 	padding: 2px;
 }
 
@@ -955,7 +964,7 @@ a.morelink:hover {
 div.form-section {
 	position: relative;
 	margin-top: 0.2em;
-	margin-bottom: 0.8em;
+	margin-bottom: 0.5em;
 }
 
 .form-section label {
@@ -970,6 +979,10 @@ label.block {
 	margin-bottom: 0.3em;
 }
 
+#task-description {
+	margin-bottom: 1em;
+}
+
 #taskedit-completeness-slider {
 	display: inline-block;
 	margin-left: 2em;
@@ -1047,7 +1060,7 @@ label.block {
 }
 
 .task-attendees span.organizer {
-	background-position: right -80px;
+	background-position: right 100px;
 }
 
 #all-task-attendees span.attendee {
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index 1881c76..727d31f 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -156,12 +156,16 @@
 		<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 id="task-attendees" class="form-section task-attendees">
+		<label><roundcube:label name="tasklist.assignedto" /></label>
+		<span class="task-text"></span>
+	</div>
+	<div id="task-organizer" class="form-section task-attendees">
+		<label><roundcube:label name="tasklist.roleorganizer" /></label>
+		<span class="task-text"></span>
 	</div>
 <!--
-	<div class="form-section" id="task-partstat">
+	<div id="task-partstat" class="form-section">
 		<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>
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 554028b..25c5796 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -81,6 +81,10 @@
 		</div>
 		<!-- attendees list (assignments) -->
 		<div id="taskedit-panel-attendees">
+			<div class="form-section" id="taskedit-organizer">
+				<label for="edit-identities-list"><roundcube:label name="tasklist.roleorganizer" /></label>
+				<roundcube:object name="plugin.identity_select" id="edit-identities-list" />
+			</div>
 			<h3 id="aria-label-attendeestable" class="voice"><roundcube:label name="tasklist.arialabeleventassignments" /></h3>
 			<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" aria-labelledby="aria-label-attendeestable" />
 			<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 03aed93..1b10773 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -84,7 +84,6 @@ function rcube_tasklist_ui(settings)
     var focused_subclass;
     var task_attendees = [];
     var attendees_list;
-//    var resources_list;
     var me = this;
 
     // general datepicker settings
@@ -1349,7 +1348,7 @@ function rcube_tasklist_ui(settings)
     };
 
     // check if the current user is an attendee of this task
-    var is_attendee = function(task, role, email)
+    var is_attendee = function(task, email, role)
     {
         var i, attendee, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
 
@@ -1366,7 +1365,10 @@ function rcube_tasklist_ui(settings)
     // check if the current user is the organizer
     var is_organizer = function(task, email)
     {
-        return is_attendee(task, 'ORGANIZER', email) || !task.id;
+        if (!email) email = task.organizer ? task.organizer.email : null;
+        if (email)
+            return settings.identity.emails.indexOf(';'+email) >= 0;
+        return true;
     };
 
     // add the given list of participants
@@ -1421,34 +1423,10 @@ function rcube_tasklist_ui(settings)
         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 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>';
@@ -1463,18 +1441,15 @@ function rcube_tasklist_ui(settings)
         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>' +
+        var html = '<td class="name">' + dispname + '</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>';
+            (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (readonly || !invbox ? '' : invbox) + '</td>' : '') +
+            '<td class="options">' + (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);
+            .appendTo(attendees_list);
 
         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);
@@ -1483,15 +1458,6 @@ function rcube_tasklist_ui(settings)
             $('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
         });
 
-        // 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;
     };
@@ -1499,15 +1465,8 @@ function rcube_tasklist_ui(settings)
     // 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);
-
-        if (rcmail.env.tasklist_resources && cutype == 'RESOURCE') {
-            task_resources_dialog(mailto);
-        }
-        else {
-            rcmail.redirect(rcmail.url('mail/compose', {_to: mailto}));
-        }
+        var mailto = this.href.substr(7);
+        rcmail.command('compose', mailto);
 
         return false;
     };
@@ -1547,7 +1506,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();
+        $('#task-attendees, #task-organizer').hide();
 
         var itags = get_inherited_tags(rec);
         var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty();
@@ -1587,6 +1546,7 @@ function rcube_tasklist_ui(settings)
 
         // list task attendees
         if (list.attendees && rec.attendees) {
+            console.log(rec.attendees)
 /*
             // sort resources to the end
             rec.attendees.sort(function(a,b) {
@@ -1595,30 +1555,19 @@ function rcube_tasklist_ui(settings)
                 return (j - k);
             });
 */
-            var j, data, dispname, tooltip, organizer = false, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '';
+            var j, data, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '',
+                organizer = is_organizer(rec);
+
             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'];
+                if (data.email && settings.identity.emails.indexOf(';'+data.email) >= 0) {
+                    mystatus = data.status.toLowerCase();
+                    if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
+                        rsvp = mystatus;
+                }
 
-                line = '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+                line = task_attendee_html(data);
 
                 if (morelink)
                     overflow += line;
@@ -1631,7 +1580,7 @@ function rcube_tasklist_ui(settings)
                 }
             }
 
-            if (html && (rec.attendees.length > 1 || !organizer)) {
+            if (html) {
                 $('#task-attendees').show()
                     .children('.task-text')
                     .html(html)
@@ -1667,6 +1616,10 @@ function rcube_tasklist_ui(settings)
             $('#task-rsvp a.reply-comment-toggle').show();
             $('#task-rsvp .itip-reply-comment textarea').hide().val('');
 */
+
+            if (rec.organizer && !organizer) {
+                $('#task-organizer').show().children('.task-text').html(task_attendee_html(rec.organizer));
+            }
         }
 
         // define dialog buttons
@@ -1697,7 +1650,7 @@ function rcube_tasklist_ui(settings)
           closeOnEscape: true,
           title: rcmail.gettext('taskdetails', 'tasklist'),
           open: function() {
-            $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+              $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
           },
           close: function() {
               $dialog.dialog('destroy').appendTo(document.body);
@@ -1711,6 +1664,24 @@ function rcube_tasklist_ui(settings)
         me.dialog_resize($dialog.get(0), $dialog.height(), 580);
     }
 
+    // render HTML code for displaying an attendee record
+    function task_attendee_html(data)
+    {
+        var 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['delegated-to'])
+          tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
+        else if (data['delegated-from'])
+          tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+
+        return '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+    }
+
     /**
      * Opens the dialog to edit a task
      */
@@ -1786,18 +1757,17 @@ function rcube_tasklist_ui(settings)
 
         task_attendees = [];
         attendees_list = $('#edit-attendees-table > tbody').html('');
-        //resources_list = $('#edit-resources-table > tbody').html('');
         $('#edit-attendees-notify')[(notify.checked && allow_invitations ? 'show' : 'hide')]();
         $('#edit-localchanges-warning')[(has_attendees(rec) && !(allow_invitations || (rec.owner && is_organizer(rec, rec.owner))) ? 'show' : 'hide')]();
 
-        var load_attendees_tab = function()
-        {
+        // attendees (aka assignees)
+        if (list.attendees) {
             var j, data, reply_selected = 0;
             if (rec.attendees) {
                 for (j=0; j < rec.attendees.length; j++) {
                     data = rec.attendees[j];
                     add_attendee(data, !allow_invitations);
-                    if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply) {
+                    if (allow_invitations && !data.noreply) {
                         reply_selected++;
                     }
                 }
@@ -1812,16 +1782,16 @@ function rcube_tasklist_ui(settings)
             // select the correct organizer identity
             var identity_id = 0;
             $.each(settings.identities, function(i,v) {
-                if (organizer && v == organizer.email) {
+                if (rec.organizer && v == rec.organizer.email) {
                     identity_id = i;
                     return false;
                 }
             });
 
-            $('#edit-identities-list').val(identity_id);
             $('#edit-attendees-form')[(allow_invitations?'show':'hide')]();
-//            $('#edit-attendee-schedule')[(tasklist.freebusy?'show':'hide')]();
-        };
+            $('#edit-identities-list').val(identity_id);
+            $('#taskedit-organizer')[(organizer ? 'show' : 'hide')]();
+        }
 
         // attachments
         rcmail.enable_command('remove-attachment', list.editable);
@@ -1904,15 +1874,9 @@ function rcube_tasklist_ui(settings)
             if (!data.tags.length)
                 data.tags = '';
 
-            // read attendee roles
-            $('select.edit-attendee-role').each(function(i, elem) {
-                if (data.attendees[i]) {
-                    data.attendees[i].role = $(elem).val();
-                }
-            });
-
             if (organizer) {
                 data._identity = $('#edit-identities-list option:selected').val();
+                delete data.organizer;
             }
 
             // don't submit attendees if only myself is added as organizer
@@ -1977,9 +1941,6 @@ function rcube_tasklist_ui(settings)
 
         // set dialog size according to content
         me.dialog_resize($dialog.get(0), $dialog.height(), 580);
-
-        if (list.attendees)
-            window.setTimeout(load_attendees_tab, 1);
     }
 
     /**
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 76ef251..0757c63 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -334,7 +334,7 @@ class tasklist extends rcube_plugin
             $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)) {
+            if (!$oldrec || $action == 'delete' || self::task_diff($task, $oldrec)) {
                 $sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']);
                 if ($sent > 0)
                     $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
@@ -366,10 +366,7 @@ class tasklist extends rcube_plugin
         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'));
-//            }
+            $this->itip->set_rsvp_actions(array('accepted','declined'));
         }
 
         return $this->itip;
@@ -520,6 +517,11 @@ class tasklist extends rcube_plugin
 
         $rec['attachments'] = $attachments;
 
+        // 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']);
+        }
+
         if (is_numeric($rec['id']) && $rec['id'] < 0)
             unset($rec['id']);
 
@@ -646,7 +648,8 @@ class tasklist extends rcube_plugin
 
         // compose multipart message using PEAR:Mail_Mime
         $method  = $action == 'delete' ? 'CANCEL' : 'REQUEST';
-        $message = $itip->compose_itip_message($task, $method);
+        $object = $this->to_libcal($task);
+        $message = $itip->compose_itip_message($object, $method);
 
         // list existing attendees from the $old task
         $old_attendees = array();
@@ -671,11 +674,11 @@ class tasklist extends rcube_plugin
 
             // 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'));
+            $bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody');
+            $subject  = $is_cancelled ? 'itipcancelsubject'  : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty'));
 
             // finally send the message
-            if ($itip->send_itip_message($task, $method, $attendee, $subject, $bodytext, $message))
+            if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message))
                 $sent++;
             else
                 $sent = -100;
@@ -683,16 +686,16 @@ class tasklist extends rcube_plugin
 
         // send CANCEL message to removed attendees
         foreach ((array)$old['attendees'] as $attendee) {
-            if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
+            if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
                 continue;
             }
 
-            $vevent = $old;
-            $vevent['cancelled'] = $is_cancelled;
-            $vevent['attendees'] = array($attendee);
-            $vevent['comment']   = $comment;
+            $vtodo = $this->to_libcal($old);
+            $vtodo['cancelled'] = $is_cancelled;
+            $vtodo['attendees'] = array($attendee);
+            $vtodo['comment']   = $comment;
 
-            if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody'))
+            if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody'))
                 $sent++;
             else
                 $sent = -100;
@@ -1393,6 +1396,8 @@ class tasklist extends rcube_plugin
 
         // successfully parsed events?
         if (!empty($tasks) && ($task = $tasks[$index])) {
+            $task = $this->from_ical($task);
+
             // store the message's sender address for comparisons
             $task['_sender'] = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : '';
             $askt['_sender_utf'] = rcube_idn_to_utf8($task['_sender']);
@@ -1496,6 +1501,7 @@ class tasklist extends rcube_plugin
             foreach ($tasks as $task) {
                 // save to tasklist
                 if ($list && $list['editable'] && $task['_type'] == 'task') {
+                    $task = $this->from_ical($task);
                     $task['list'] = $list['id'];
 
                     if (!$this->driver->get_task($task['uid'])) {
@@ -1555,14 +1561,11 @@ class tasklist extends rcube_plugin
 
             // update my attendee status according to submitted method
             if (!empty($status)) {
-                $organizer = null;
+                $organizer = $task['organizer'];
                 $emails    = $this->lib->get_user_emails();
 
                 foreach ($task['attendees'] as $i => $attendee) {
-                    if ($attendee['role'] == 'ORGANIZER') {
-                        $organizer = $attendee;
-                    }
-                    else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+                    if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
                         $metadata['attendee'] = $attendee['email'];
                         $metadata['rsvp']     = $attendee['role'] != 'NON-PARTICIPANT';
                         $reply_sender         = $attendee['email'];
@@ -1714,7 +1717,7 @@ class tasklist extends rcube_plugin
             $itip = $this->load_itip();
             $itip->set_sender_email($reply_sender);
 
-            if ($itip->send_itip_message($task, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
+            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');
             else
                 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
@@ -1729,7 +1732,7 @@ class tasklist extends rcube_plugin
     /**
      * Handler for calendar/itip-status requests
      */
-    function task_itip_status()
+    public function task_itip_status()
     {
         $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
 
@@ -1768,7 +1771,7 @@ class tasklist extends rcube_plugin
     /**
      * Handler for calendar/itip-remove requests
      */
-    function task_itip_remove()
+    public function task_itip_remove()
     {
         $success = false;
         $uid     = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
@@ -1798,6 +1801,78 @@ class tasklist extends rcube_plugin
     }
 
     /**
+     * Map task properties for ical exprort using libcalendaring
+     */
+    public function to_libcal($task)
+    {
+        $object = $task;
+        $object['categories'] = (array)$task['tags'];
+
+        // convert to datetime objects
+        if (!empty($task['date'])) {
+            $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->timezone);
+            if (empty($task['time']))
+                $object['due']->_dateonly = true;
+            unset($object['date']);
+        }
+
+        if (!empty($task['startdate'])) {
+            $object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->timezone);
+            if (empty($task['starttime']))
+                $object['start']->_dateonly = true;
+            unset($object['startdate']);
+        }
+
+        $object['complete'] = $task['complete'] * 100;
+        if ($task['complete'] == 1.0 && empty($task['complete'])) {
+            $object['status'] = 'COMPLETED';
+        }
+
+        if ($task['flagged']) {
+            $object['priority'] = 1;
+        }
+        else if (!$task['priority']) {
+            $object['priority'] = 0;
+        }
+
+        return $object;
+    }
+
+    /**
+     * Convert task properties from ical parser to the internal format
+     */
+    public function from_ical($vtodo)
+    {
+        $task = $vtodo;
+
+        $task['tags'] = array_filter((array)$vtodo['categories']);
+        $task['flagged'] = $vtodo['priority'] == 1;
+        $task['complete'] = floatval($vtodo['complete'] / 100);
+
+        // convert from DateTime to internal date format
+        if (is_a($vtodo['due'], 'DateTime')) {
+            $due = $this->lib->adjust_timezone($vtodo['due']);
+            $task['date'] = $due->format('Y-m-d');
+            if (!$vtodo['due']->_dateonly)
+                $task['time'] = $due->format('H:i');
+        }
+        // convert from DateTime to internal date format
+        if (is_a($vtodo['start'], 'DateTime')) {
+            $start = $this->lib->adjust_timezone($vtodo['start']);
+            $task['startdate'] = $start->format('Y-m-d');
+            if (!$vtodo['start']->_dateonly)
+                $task['starttime'] = $start->format('H:i');
+        }
+        if (is_a($vtodo['dtstamp'], 'DateTime')) {
+            $task['changed'] = $vtodo['dtstamp'];
+        }
+
+        unset($task['categories'], $task['due'], $task['start'], $task['dtstamp']);
+
+        return $task;
+    }
+
+    /**
      * Handler for user_delete plugin hook
      */
     public function user_delete($args)
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 5fc0a20..21b322e 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -56,7 +56,6 @@ 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();
@@ -71,7 +70,7 @@ class tasklist_ui
     {
         $settings = array();
 
-        //$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
+        $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0);
 
         // get user identity to create default attendee
         foreach ($this->rc->user->list_identities() as $rec) {
@@ -128,6 +127,7 @@ class tasklist_ui
         $this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
         $this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
         $this->plugin->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
+        $this->plugin->register_handler('plugin.identity_select', array($this, 'identity_select'));
         $this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
 
         $this->plugin->include_script('jquery.tagedit.js');
@@ -438,9 +438,8 @@ class tasklist_ui
         $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
         $table  = new html_table(array('cols' => 4 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
 
-        $table->add_header('role', $this->plugin->gettext('role'));
+//      $table->add_header('role', $this->plugin->gettext('role'));
         $table->add_header('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee'));
-//        $table->add_header('availability', $this->plugin->gettext('availability'));
         $table->add_header('confirmstate', $this->plugin->gettext('confirmstate'));
         if ($invitations) {
             $table->add_header(array('class' => 'sendmail', 'title' => $this->plugin->gettext('sendinvitations')),





More information about the commits mailing list