diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 1999a8e5..624bdd56 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 eb7663e5..dd2e415f 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 7920c940..0b713774 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 00000000..59b44930
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 00000000..ecdaa091
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 1e847b35..7bdfe570 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 3003067a..1881c762 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -156,6 +156,19 @@
+
+
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 599974e3..554028b3 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -98,5 +98,5 @@
-
+
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index b0c95baf..03aed934 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*$/, ''), ',');
+ 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]);
+ // 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'));
- }
- }
+ 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<>]+$/, '');
+ }
- return success;
+ if (email) {
+ add_attendee($.extend({ email:email, name:name }, params));
+ success = true;
+ }
+ else {
+ alert(rcmail.gettext('noemailwarning'));
+ }
+ }
+
+ 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 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 = '' + dispname + '';
+ var dispname = Q(data.name || data.email);
+ if (data.email)
+ dispname = '' + dispname + '';
- // 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');
+ // 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 (data.cutype != 'RESOURCE')
+ opts['CHAIR'] = rcmail.gettext('tasklist.rolechair');
- if (organizer && !readonly)
- dispname = rcmail.env['identities-selector'];
+ if (organizer && !readonly)
+ dispname = rcmail.env['identities-selector'];
- var select = '';
+ var select = '';
- // availability
- var avail = data.email ? 'loading' : 'unknown';
+ // availability
+ var avail = data.email ? 'loading' : 'unknown';
- // delete icon
- var icon = rcmail.env.deleteicon ? '
' : rcmail.gettext('delete');
- var dellink = '' + icon + '';
- var tooltip = data.status || '';
+ // delete icon
+ var icon = rcmail.env.deleteicon ? '
' : rcmail.gettext('delete');
+ var dellink = '' + icon + '';
+ var tooltip = data.status || '';
- // send invitation checkbox
- var invbox = '';
+ // send invitation checkbox
+ var invbox = '';
- 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['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 = '' + select + ' | ' +
- '' + dispname + ' | ' +
-// ' | ' +
- '' + Q(data.status || '') + ' | ' +
- (data.cutype != 'RESOURCE' ? '' + (organizer || readonly || !invbox ? '' : invbox) + ' | ' : '') +
- '' + (organizer || readonly ? '' : dellink) + ' | ';
+ var html = '' + select + ' | ' +
+ '' + dispname + ' | ' +
+// ' | ' +
+ '' + Q(data.status || '') + ' | ' +
+ (data.cutype != 'RESOURCE' ? '' + (organizer || readonly || !invbox ? '' : invbox) + ' | ' : '') +
+ '' + (organizer || readonly ? '' : dellink) + ' | ';
- var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
- var tr = $('')
- .addClass(String(data.role).toLowerCase())
- .html(html)
- .appendTo(table);
+ var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
+ var 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']();
- });
+ 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 = '' + dispname + '';
+ 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 = '' + dispname + ' ';
+
+ if (morelink)
+ overflow += line;
+ else
+ html += line;
+
+ // stop listing attendees
+ if (j == 7 && rec.attendees.length >= 7) {
+ morelink = $('').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(
+ '' + html + overflow + '
',
+ 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 d227f548..34b1ffb9 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');
@@ -336,6 +353,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
*/
@@ -586,6 +620,106 @@ class tasklist extends rcube_plugin
return $clone;
}
+ /**
+ * 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
*/
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 4dd7b82b..5fc0a207 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();
@@ -91,6 +92,22 @@ class tasklist_ui
return $settings;
}
+ /**
+ * 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
*/
@@ -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;
}