12 commits - plugins/calendar plugins/libcalendaring plugins/libkolab plugins/tasklist
Thomas Brüderli
bruederli at kolabsys.com
Thu Jul 31 13:50:21 CEST 2014
plugins/calendar/calendar.php | 25
plugins/libcalendaring/lib/libcalendaring_itip.php | 37
plugins/libcalendaring/libcalendaring.php | 18
plugins/libcalendaring/libvcalendar.php | 2
plugins/libcalendaring/localization/en_US.inc | 8
plugins/libkolab/lib/kolab_format_event.php | 2
plugins/libkolab/lib/kolab_format_task.php | 2
plugins/libkolab/lib/kolab_format_xcal.php | 5
plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php | 26
plugins/tasklist/drivers/tasklist_driver.php | 1
plugins/tasklist/localization/en_US.inc | 73 +
plugins/tasklist/skins/larry/images/attendee-status.png |binary
plugins/tasklist/skins/larry/images/badge_cancelled.png |binary
plugins/tasklist/skins/larry/images/loading_blue.gif |binary
plugins/tasklist/skins/larry/images/sendinvitation.png |binary
plugins/tasklist/skins/larry/images/tasklist.png |binary
plugins/tasklist/skins/larry/tasklist.css | 277 ++++-
plugins/tasklist/skins/larry/templates/mainview.html | 17
plugins/tasklist/skins/larry/templates/taskedit.html | 14
plugins/tasklist/tasklist.js | 608 +++++++++--
plugins/tasklist/tasklist.php | 828 ++++++++++++++-
plugins/tasklist/tasklist_base.js | 35
plugins/tasklist/tasklist_ui.php | 111 +-
23 files changed, 1969 insertions(+), 120 deletions(-)
New commits:
commit 65989e7783942506873a0f554cf82cbb6c28188b
Merge: 93ce0d8 f5e9318
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 13:49:47 2014 +0200
Merge branch 'dev/task-attendees'
commit f5e93184e3ef08d2e4f9d57f3ade9a41390fe610
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 12:49:27 2014 +0200
Fix iTip replies to organizer (don't increment sequence if I'm not the owner)
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index ef8f31d..bcc5c06 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -902,9 +902,15 @@ class tasklist_kolab_driver extends tasklist_driver
unset($object['attachments']);
}
- $object['_owner'] = $identity['email'];
+ // allow sequence increments if I'm the organizer
+ if ($this->plugin->is_organizer($object)) {
+ unset($object['sequence']);
+ }
+ else if (isset($old['sequence'])) {
+ $object['sequence'] = $old['sequence'];
+ }
- unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['sequence']);
+ unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
return $object;
}
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 3953977..96b5e82 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -949,10 +949,11 @@ a.morelink:hover {
display: block;
color: #333;
font-weight: bold;
- padding: 8px 4px 3px 30px;
+ padding: 3px 4px 3px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
+ line-height: 20px;
}
#taskedit-attachments ul li a.file {
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 9da7a88..a0ff22f 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -992,16 +992,14 @@ function rcube_tasklist_ui(settings)
notify = false, partstat = false, html = '';
// task has attendees, ask whether to notify them
- if (has_attendees(rec)) {
- if (is_organizer(rec)) {
- notify = true;
- html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
- }
- // ask whether to change my partstat and notify organizer
- else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
- partstat = true;
- html = rcmail.gettext('partstatupdatenotification', 'tasklist');
- }
+ if (has_attendees(rec) && is_organizer(rec)) {
+ notify = true;
+ html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
+ }
+ // ask whether to change my partstat and notify organizer
+ else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
+ partstat = true;
+ html = rcmail.gettext('partstatupdatenotification', 'tasklist');
}
// remove to avoid endless recursion
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index ae76f99..728c4a3 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1097,6 +1097,16 @@ class tasklist extends rcube_plugin
return false;
}
+ /**
+ * Determine whether the current user is the organizer of the given task
+ */
+ public function is_organizer($task)
+ {
+ $emails = $this->lib->get_user_emails();
+ return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
+ }
+
+
/******* UI functions ********/
/**
commit f8b6706074b394244b52d479e90dacd99f6c6cce
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 12:43:55 2014 +0200
Fix 'create task from mail' function
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 9ac70bc..ae76f99 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1128,11 +1128,17 @@ class tasklist extends rcube_plugin
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
+ // collect env variables
+ $env = array(
+ 'tasklists' => array(),
+ 'tasklist_settings' => $this->ui->load_settings(),
+ );
+
$this->ui->init_templates();
echo $this->api->output->parse('tasklist.taskedit', false, false);
echo html::tag('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => $this->url($this->local_skin_path() . '/tagedit.css'), 'nl' => true));
echo html::tag('script', array('type' => 'text/javascript'),
- "rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
+ "rcmail.set_env(" . json_encode($env) . ");\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js
index 81e27f0..5448418 100644
--- a/plugins/tasklist/tasklist_base.js
+++ b/plugins/tasklist/tasklist_base.js
@@ -57,7 +57,7 @@ function rcube_tasklist(settings)
// rcmail.gui_object('attachmentlist', 'attachmentlist');
ui_loaded = true;
- me.ui = new rcube_tasklist_ui(settings);
+ me.ui = new rcube_tasklist_ui($.extend(rcmail.env.tasklist_settings, settings));
create_from_mail(uid); // start over
});
return;
commit 0d8b6912ae2864113a520b5f81a0d63447ecf8ff
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 12:22:07 2014 +0200
Fix auto-incrementing the sequence value of xcal objects
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 596d0da..c233f44 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -26,7 +26,7 @@ class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
- public static $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+ public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
protected $objclass = 'Event';
protected $read_func = 'readEvent';
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index ee0ca6a..52744d4 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -26,7 +26,7 @@ class kolab_format_task extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.task';
- public static $scheduling_properties = array('start', 'due', 'summary', 'status');
+ public $scheduling_properties = array('start', 'due', 'summary', 'status');
protected $objclass = 'Todo';
protected $read_func = 'readTodo';
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 6624b02..7d077b7 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -29,7 +29,8 @@ abstract class kolab_format_xcal extends kolab_format
public $CTYPE = 'application/calendar+xml';
public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
- public static $scheduling_properties = array('start', 'end', 'location');
+
+ public $scheduling_properties = array('start', 'end', 'location');
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
@@ -315,7 +316,7 @@ abstract class kolab_format_xcal extends kolab_format
// increment sequence when updating properties relevant for scheduling.
// RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
// TODO: make the list of properties considered 'significant' for scheduling configurable
- foreach (self::$scheduling_properties as $prop) {
+ foreach ($this->scheduling_properties as $prop) {
$a = $old[$prop];
$b = $object[$prop];
if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 6884083..ef8f31d 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -904,7 +904,7 @@ class tasklist_kolab_driver extends tasklist_driver
$object['_owner'] = $identity['email'];
- unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
+ unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['sequence']);
return $object;
}
commit 457195102e070c098899da7972b2fe6a25f572aa
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 11:40:39 2014 +0200
Complete iTip communication on task status changes: ask to notify the organizer on update or deletion + add icons for task-specific partstats and cancelled tasks
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index b37faa8..a7ec1ea 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -51,6 +51,7 @@ $labels['newtask'] = 'New Task';
$labels['edittask'] = 'Edit Task';
$labels['save'] = 'Save';
$labels['cancel'] = 'Cancel';
+$labels['saveandnotify'] = 'Save and Notify';
$labels['addsubtask'] = 'Add subtask';
$labels['deletetask'] = 'Delete task';
$labels['deletethisonly'] = 'Delete this task only';
@@ -88,6 +89,9 @@ $labels['deleteparenttasktconfirm'] = 'Do you really want to delete this task an
$labels['deletelistconfirm'] = 'Do you really want to delete this list with all its tasks?';
$labels['deletelistconfirmrecursive'] = 'Do you really want to delete this list with all its sub-lists and tasks?';
$labels['aclnorights'] = 'You do not have administrator rights on this task list.';
+$labels['changetaskconfirm'] = 'Update task';
+$labels['changeconfirmnotifications'] = 'Do you want to notify the attendees about the modification?';
+$labels['partstatupdatenotification'] = 'Do you want to notify the organizer about the status change?';
// (hidden) titles and labels for accessibility annotations
$labels['quickaddinput'] = 'New task date and title';
@@ -124,17 +128,27 @@ $labels['saveintasklist'] = 'save in ';
// invitation handling (overrides labels from libcalendaring)
$labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
-$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nDue: \$date";
+$labels['itipmailbodyin-process'] = "\$sender has set the status of the following task to in-process:\n\n*\$title*\n\nDue: \$date";
+$labels['itipmailbodycompleted'] = "\$sender has completed the following task:\n\n*\$title*\n\nDue: \$date";
-$labels['itipdeclineevent'] = 'Do you want to decline your assignment to this task?';
+$labels['attendeeaccepted'] = 'Assignee has accepted';
+$labels['attendeetentative'] = 'Assignee has tentatively accepted';
+$labels['attendeedeclined'] = 'Assignee has declined';
+$labels['attendeedelegated'] = 'Assignee has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Assignee is in-process';
+$labels['attendeecompleted'] = 'Assignee has completed';
+
+$labels['itipdeclinetask'] = 'Decline your assignment to this task to the organizer';
$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
$labels['itipcomment'] = 'Invitation/notification comment';
-$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants';
-$labels['itipsendsuccess'] = 'Invitation sent to participants.';
-$labels['errornotifying'] = 'Failed to send notifications to task participants';
+$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to assignees';
+$labels['itipsendsuccess'] = 'Invitation sent to assignees';
+$labels['errornotifying'] = 'Failed to send notifications to task assignees';
+$labels['removefromcalendar'] = 'Remove from my tasks';
$labels['andnmore'] = '$nr more...';
$labels['delegatedto'] = 'Delegated to: ';
@@ -149,7 +163,7 @@ $labels['nowritetasklistfound'] = 'No tasklist found to save the task';
$labels['importedsuccessfully'] = 'The task was successfully added to \'$list\'';
$labels['updatedsuccessfully'] = 'The task was successfully updated in \'$list\'';
$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
-$labels['itipresponseerror'] = 'Failed to send the response to this task invitation';
+$labels['itipresponseerror'] = 'Failed to send the response to this task assignment';
$labels['itipinvalidrequest'] = 'This invitation is no longer valid';
-$labels['sentresponseto'] = 'Successfully sent invitation response to $mailto';
+$labels['sentresponseto'] = 'Successfully sent assignment response to $mailto';
$labels['successremoval'] = 'The task has been deleted successfully.';
diff --git a/plugins/tasklist/skins/larry/images/attendee-status.png b/plugins/tasklist/skins/larry/images/attendee-status.png
index 59b4493..5343e60 100644
Binary files a/plugins/tasklist/skins/larry/images/attendee-status.png and b/plugins/tasklist/skins/larry/images/attendee-status.png differ
diff --git a/plugins/tasklist/skins/larry/images/badge_cancelled.png b/plugins/tasklist/skins/larry/images/badge_cancelled.png
new file mode 100644
index 0000000..b89029e
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/badge_cancelled.png differ
diff --git a/plugins/tasklist/skins/larry/images/ical-attachment.png b/plugins/tasklist/skins/larry/images/ical-attachment.png
deleted file mode 100644
index 8fa486a..0000000
Binary files a/plugins/tasklist/skins/larry/images/ical-attachment.png and /dev/null differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 1d891ed..3953977 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -815,6 +815,10 @@ ul.toolbarmenu li span.delete {
color: #999;
}
+#taskshow.status-cancelled {
+ background: url(images/badge_cancelled.png) top right no-repeat;
+}
+
#task-parent-title {
position: relative;
top: -0.6em;
@@ -1043,7 +1047,7 @@ label.block {
text-decoration: underline;
}
-.task-attendees span.accepted {
+.task-attendees span.completed {
background-position: right -20px;
}
@@ -1059,6 +1063,14 @@ label.block {
background-position: right -180px;
}
+.task-attendees span.in-process {
+ background-position: right -200px;
+}
+
+.task-attendees span.accepted {
+ background-position: right -220px;
+}
+
.task-attendees span.organizer {
background-position: right 100px;
}
@@ -1082,12 +1094,7 @@ label.block {
width: 20em;
}
-.ui-dialog .task-update-confirm {
- padding: 0 0.5em 0.5em 0.5em;
-}
-
-.task-dialog-message,
-.task-update-confirm .message {
+.task-dialog-message {
margin-top: 0.5em;
padding: 0.8em;
border: 1px solid #ffdf0e;
@@ -1161,35 +1168,54 @@ div.tasklist-invitebox .rsvp-status.hint {
}
#event-partstat .changersvp,
+.edit-attendees-table td.confirmstate span,
div.tasklist-invitebox .rsvp-status.declined,
div.tasklist-invitebox .rsvp-status.tentative,
div.tasklist-invitebox .rsvp-status.accepted,
div.tasklist-invitebox .rsvp-status.delegated,
-div.tasklist-invitebox .rsvp-status.needs-action {
+div.tasklist-invitebox .rsvp-status.in-process,
+div.tasklist-invitebox .rsvp-status.completed,
+div.tasklist-invitebox .rsvp-status.needs-action {
padding: 0 0 1px 22px;
background: url(images/attendee-status.png) 2px -20px no-repeat;
}
#event-partstat .changersvp.declined,
-div.tasklist-invitebox .rsvp-status.declined {
+div.tasklist-invitebox .rsvp-status.declined,
+.edit-attendees-table td.confirmstate span.declined {
background-position: 2px -40px;
}
#event-partstat .changersvp.tentative,
-div.tasklist-invitebox .rsvp-status.tentative {
+div.tasklist-invitebox .rsvp-status.tentative,
+.edit-attendees-table td.confirmstate span.tentative {
background-position: 2px -60px;
}
#event-partstat .changersvp.delegated,
-div.tasklist-invitebox .rsvp-status.delegated {
+div.tasklist-invitebox .rsvp-status.delegated,
+.edit-attendees-table td.confirmstate span.delegated {
background-position: 2px -180px;
}
#event-partstat .changersvp.needs-action,
-div.tasklist-invitebox .rsvp-status.needs-action {
+div.tasklist-invitebox .rsvp-status.needs-action,
+.edit-attendees-table td.confirmstate span.needs-action {
background-position: 2px 0;
}
+#event-partstat .changersvp.in-process,
+div.tasklist-invitebox .rsvp-status.in-process,
+.edit-attendees-table td.confirmstate span.in-process {
+ background-position: 2px -200px;
+}
+
+#event-partstat .changersvp.accepted,
+div.tasklist-invitebox .rsvp-status.accepted,
+.edit-attendees-table td.confirmstate span.accepted {
+ background-position: 2px -220px;
+}
+
/** Special hacks for IE7 **/
/** They need to be in this file to also affect the task-create dialog embedded in mail view **/
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 1b10773..9da7a88 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -353,9 +353,8 @@ function rcube_tasklist_ui(settings)
if (rcmail.busy)
return false;
- rec.status = e.target.checked ? 'COMPLETED' : (rec.complete == 1 ? 'NEEDS-ACTION' : '');
- li.toggleClass('complete');
- save_task(rec, 'edit');
+ save_task_confirm(rec, 'edit', { _status_before:rec.status + '', status:e.target.checked ? 'COMPLETED' : (rec.complete > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION') });
+ item.toggleClass('complete');
return true;
case 'flagged':
@@ -363,7 +362,7 @@ function rcube_tasklist_ui(settings)
return false;
rec.flagged = rec.flagged ? 0 : 1;
- li.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
+ item.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
save_task(rec, 'edit');
break;
@@ -377,8 +376,7 @@ function rcube_tasklist_ui(settings)
input.datepicker($.extend({
onClose: function(dateText, inst) {
if (dateText != (rec.date || '')) {
- rec.date = dateText;
- save_task(rec, 'edit');
+ save_task_confirm(rec, 'edit', { date:dateText });
}
input.datepicker('destroy').remove();
link.html(dateText || rcmail.gettext('nodate','tasklist'));
@@ -971,6 +969,10 @@ function rcube_tasklist_ui(settings)
*/
function save_task(rec, action)
{
+ // show confirmation dialog when status of an assigned task has changed
+ if (rec._status_before !== undefined && is_attendee(rec))
+ return save_task_confirm(rec, action);
+
if (!rcmail.busy) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask });
@@ -982,6 +984,84 @@ function rcube_tasklist_ui(settings)
}
/**
+ * Display confirm dialog when modifying/deleting a task record
+ */
+ var save_task_confirm = function(rec, action, updates)
+ {
+ var data = $.extend({}, rec, updates || {}),
+ notify = false, partstat = false, html = '';
+
+ // task has attendees, ask whether to notify them
+ if (has_attendees(rec)) {
+ if (is_organizer(rec)) {
+ notify = true;
+ html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
+ }
+ // ask whether to change my partstat and notify organizer
+ else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
+ partstat = true;
+ html = rcmail.gettext('partstatupdatenotification', 'tasklist');
+ }
+ }
+
+ // remove to avoid endless recursion
+ delete data._status_before;
+
+ // show dialog
+ if (html) {
+ var $dialog = $('<div>').html(html);
+
+ var buttons = [];
+ buttons.push({
+ text: rcmail.gettext('saveandnotify', 'tasklist'),
+ click: function() {
+ if (notify) data._notify = 1;
+ if (partstat) data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status;
+ save_task(data, action);
+ $(this).dialog('close');
+ }
+ });
+ buttons.push({
+ text: rcmail.gettext('save', 'tasklist'),
+ click: function() {
+ save_task(data, action);
+ $(this).dialog('close');
+ }
+ });
+ buttons.push({
+ text: rcmail.gettext('cancel', 'tasklist'),
+ click: function() {
+ $(this).dialog('close');
+ if (updates)
+ render_task(rec, rec.id); // restore previous state
+ }
+ });
+
+ $dialog.dialog({
+ modal: true,
+ width: 460,
+ closeOnEscapeType: false,
+ dialogClass: 'warning no-close',
+ title: rcmail.gettext('changetaskconfirm', 'tasklist'),
+ buttons: buttons,
+ open: function() {
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function(){
+ $dialog.dialog('destroy').remove();
+ }
+ }).addClass('task-update-confirm').show();
+
+ return true;
+ }
+
+ // do update
+ return save_task(data, action);
+ }
+
+ /**
* Remove saving lock and free the UI for new input
*/
function unlock_saving()
@@ -1488,6 +1568,12 @@ function rcube_tasklist_ui(settings)
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
+ // remove status-* classes
+ $dialog.removeClass(function(i, oldclass) {
+ var oldies = String(oldclass).split(' ');
+ return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' ');
+ });
+
if (!(rec = listdata[id]) || clear_popups({}))
return;
@@ -1528,6 +1614,10 @@ function rcube_tasklist_ui(settings)
});
}
+ if (rec.status) {
+ $dialog.addClass('status-' + String(rec.status).toLowerCase());
+ }
+
if (rec.recurrence && rec.recurrence_text) {
$('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
}
@@ -1817,6 +1907,7 @@ function rcube_tasklist_ui(settings)
var buttons = {};
buttons[rcmail.gettext('save', 'tasklist')] = function() {
var data = me.selected_task;
+ data._status_before = me.selected_task.status + '';
// copy form field contents into task object to save
$.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus, list:tasklist }, function(key,input){
@@ -1867,6 +1958,8 @@ function rcube_tasklist_ui(settings)
data.complete = complete.val() / 100;
if (isNaN(data.complete))
data.complete = null;
+ else if (data.complete == 1.0 && rec.status === '')
+ data.status = 'COMPLETED';
if (!data.list && list.id)
data.list = list.id;
@@ -1879,11 +1972,6 @@ function rcube_tasklist_ui(settings)
delete data.organizer;
}
- // don't submit attendees if only myself is added as organizer
- if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && String(data.attendees[0].email).toLowerCase() == settings.identity.email) {
- data.attendees = [];
- }
-
// per-attendee notification suppression
var need_invitation = false;
if (allow_invitations) {
@@ -2050,7 +2138,34 @@ function rcube_tasklist_ui(settings)
if (!rec || rec.readonly || rcmail.busy)
return false;
- var html, buttons = [];
+ var html, buttons = [], $dialog = $('<div>');
+
+ // Subfunction to submit the delete command after confirm
+ var _delete_task = function(id, mode) {
+ var rec = listdata[id],
+ li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide(),
+ decline = $dialog.find('input.confirm-attendees-decline:checked').length,
+ notify = $dialog.find('input.confirm-attendees-notify:checked').length;
+
+ saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
+ rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list, _decline:decline, _notify:notify }, mode:mode, filter:filtermask });
+
+ // move childs to parent/root
+ if (mode != 1 && rec.children !== undefined) {
+ var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
+ if (!parent_node || !parent_node.length)
+ parent_node = rcmail.gui_objects.resultlist;
+
+ $.each(rec.children, function(i,cid) {
+ var child = listdata[cid];
+ child.parent_id = rec.parent_id;
+ resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
+ });
+ }
+
+ li.remove();
+ delete listdata[id];
+ }
if (rec.children && rec.children.length) {
html = rcmail.gettext('deleteparenttasktconfirm','tasklist');
@@ -2080,6 +2195,19 @@ function rcube_tasklist_ui(settings)
});
}
+ if (is_attendee(rec)) {
+ html += '<div class="task-dialog-message">' +
+ '<label><input class="confirm-attendees-decline" type="checkbox" checked="checked" value="1" name="_decline" /> ' +
+ rcmail.gettext('itipdeclinetask', 'tasklist') +
+ '</label></div>';
+ }
+ else if (has_attendees(rec) && is_organizer(rec)) {
+ html += '<div class="task-dialog-message">' +
+ '<label><input class="confirm-attendees-notify" type="checkbox" checked="checked" value="1" name="_notify" /> ' +
+ rcmail.gettext('sendcancellation', 'tasklist') +
+ '</label></div>';
+ }
+
buttons.push({
text: rcmail.gettext('cancel', 'tasklist'),
click: function() {
@@ -2087,11 +2215,11 @@ function rcube_tasklist_ui(settings)
}
});
- var $dialog = $('<div>').html(html);
+ $dialog.html(html);
$dialog.dialog({
modal: true,
width: 520,
- dialogClass: 'warning',
+ dialogClass: 'warning no-close',
title: rcmail.gettext('deletetask', 'tasklist'),
buttons: buttons,
close: function(){
@@ -2103,34 +2231,6 @@ function rcube_tasklist_ui(settings)
}
/**
- * Subfunction to submit the delete command after confirm
- */
- function _delete_task(id, mode)
- {
- var rec = listdata[id],
- li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
-
- saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
- rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list }, mode:mode, filter:filtermask });
-
- // move childs to parent/root
- if (mode != 1 && rec.children !== undefined) {
- var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
- if (!parent_node || !parent_node.length)
- parent_node = rcmail.gui_objects.resultlist;
-
- $.each(rec.children, function(i,cid) {
- var child = listdata[cid];
- child.parent_id = rec.parent_id;
- resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
- });
- }
-
- li.remove();
- delete listdata[id];
- }
-
- /**
* Check if the given task matches the current filtermask and tag selection
*/
function match_filter(rec, cache, recursive)
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 62ddea0..9ac70bc 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -342,6 +342,24 @@ class tasklist extends rcube_plugin
$this->rc->output->show_message('tasklist.errornotifying', 'error');
}
}
+ else if ($success && $rec['_reportpartstat']) {
+ // get the full record after update
+ $task = $this->driver->get_task($rec);
+
+ // send iTip REPLY with the updated partstat
+ if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) {
+ $sender = $task['attendees'][$idx];
+ $status = strtolower($sender['status']);
+
+ $itip = $this->load_itip();
+ $itip->set_sender_email($sender['email']);
+
+ if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
+ else
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+ }
// unlock client
$this->rc->output->command('plugin.unlock_saving');
@@ -367,6 +385,7 @@ class tasklist extends rcube_plugin
require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
$this->itip = new libcalendaring_itip($this, 'tasklist');
$this->itip->set_rsvp_actions(array('accepted','declined'));
+ $this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
}
return $this->itip;
@@ -517,6 +536,20 @@ class tasklist extends rcube_plugin
$rec['attachments'] = $attachments;
+ // convert invalid data
+ if (isset($rec['attendees']) && !is_array($rec['attendees']))
+ $rec['attendees'] = array();
+
+ // copy the task status to my attendee partstat
+ if (!empty($rec['_reportpartstat'])) {
+ if (($idx = $this->is_attendee($rec)) !== false) {
+ if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED'))
+ $rec['attendees'][$idx]['status'] = $rec['_reportpartstat'];
+ else
+ unset($rec['_reportpartstat']);
+ }
+ }
+
// set organizer from identity selector
if (isset($rec['_identity']) && ($identity = $this->rc->user->get_identity($rec['_identity']))) {
$rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']);
@@ -1044,9 +1077,25 @@ class tasklist extends rcube_plugin
else if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit))
$mask |= self::FILTER_MASK_LATER;
+ // TODO: add mask for "assigned to me"
+
return $mask;
}
+ /**
+ * Determine whether the current user is an attendee of the given task
+ */
+ public function is_attendee($task)
+ {
+ $emails = $this->lib->get_user_emails();
+ foreach ((array)$task['attendees'] as $i => $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ return $i;
+ }
+ }
+
+ return false;
+ }
/******* UI functions ********/
@@ -1709,7 +1758,7 @@ class tasklist extends rcube_plugin
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
$metadata['rsvp'] = intval($metadata['rsvp']);
- $metadata['after_action'] = $this->rc->config->get('tasklist_itip_after_action');
+ $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
$this->rc->output->command('plugin.itip_message_processed', $metadata);
$error_msg = null;
@@ -1725,7 +1774,7 @@ class tasklist extends rcube_plugin
$itip->set_sender_email($reply_sender);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
- $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
@@ -1813,6 +1862,7 @@ class tasklist extends rcube_plugin
public function to_libcal($task)
{
$object = $task;
+ $object['_type'] = 'task';
$object['categories'] = (array)$task['tags'];
// convert to datetime objects
commit e46cc9499efb8539be4a8701155c32fabbd81d28
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 11:36:18 2014 +0200
Add support for task-specific participant status values
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index ece0e48..93bdfee 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -60,6 +60,11 @@ class libcalendaring_itip
$this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
}
+ public function set_rsvp_status($status)
+ {
+ $this->rsvp_status = $status;
+ }
+
/**
* Wrapper for rcube_plugin::gettext()
* Checking for a label in different domains
@@ -246,7 +251,8 @@ class libcalendaring_itip
// attach ics file for this event
$ical = libcalendaring::get_ical();
$ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
- $message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
+ $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
+ $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
return $message;
}
@@ -321,9 +327,10 @@ class libcalendaring_itip
$listed = false;
foreach ($existing['attendees'] as $attendee) {
if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
- if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED','DELEGATED'))) {
- $html = html::div('rsvp-status ' . strtolower($status), $this->gettext(array(
- 'name' => 'attendee'.strtolower($status),
+ $status_lc = strtolower($status);
+ if (in_array($status_lc, $this->rsvp_status)) {
+ $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
+ 'name' => 'attendee' . $status_lc,
'vars' => array(
'delegatedto' => Q($attendee['delegated-to'] ?: '?'),
)
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 7e8c717..f3a506b 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -86,6 +86,8 @@ $labels['itipobjectnotfound'] = 'The object referred by this message was not fou
$labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
$labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
$labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
+$labels['itipsubjectin-process'] = '"$title" is in-process by $name';
+$labels['itipsubjectcompleted'] = '"$title" was completed by $name';
$labels['itipsubjectcancel'] = 'Your participation in "$title" has been cancelled';
$labels['itipnewattendee'] = 'This is a reply from a new participant';
@@ -99,18 +101,24 @@ $labels['youhaveaccepted'] = 'You have accepted this invitation';
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
$labels['youhavedeclined'] = 'You have declined this invitation';
$labels['youhavedelegated'] = 'You have delegated this invitation';
+$labels['youhavein-process'] = 'You are working on this assignment';
+$labels['youhavecompleted'] = 'You have completed this assignment';
$labels['youhaveneeds-action'] = 'Your response to this invitation is still pending';
$labels['youhavepreviouslyaccepted'] = 'You have previously accepted this invitation';
$labels['youhavepreviouslytentative'] = 'You have previously accepted this invitation tentatively';
$labels['youhavepreviouslydeclined'] = 'You have previously declined this invitation';
$labels['youhavepreviouslydelegated'] = 'You have previously delegated this invitation';
+$labels['youhavepreviouslyin-process'] = 'You have previously reported to work on this assignment';
+$labels['youhavepreviouslycompleted'] = 'You have previously completed this assignment';
$labels['youhavepreviouslyneeds-action'] = 'Your response to this invitation is still pending';
$labels['attendeeaccepted'] = 'Participant has accepted';
$labels['attendeetentative'] = 'Participant has tentatively accepted';
$labels['attendeedeclined'] = 'Participant has declined';
$labels['attendeedelegated'] = 'Participant has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Participant is in-process';
+$labels['attendeecompleted'] = 'Participant has completed';
$labels['notanattendee'] = 'You\'re not listed as an attendee of this object';
$labels['outdatedinvitation'] = 'This invitation has been replaced by a newer version';
commit 228a1b2438e507a002e106f18638c9bd44e77fea
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Wed Jul 30 17:51:54 2014 +0200
Hide 'Save to calendar' option for VTODO attachments (#3227)
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 343b3e5..4768048 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2240,6 +2240,7 @@ class calendar extends rcube_plugin
}
$html = '';
+ $has_events = false;
foreach ($this->ics_parts as $mime_id) {
$part = $this->message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
@@ -2255,16 +2256,20 @@ class calendar extends rcube_plugin
if ($event['_type'] != 'event') // skip non-event objects (#2928)
continue;
+ $has_events = true;
+
// get prepared inline UI for this event object
- $html .= html::div('calendar-invitebox',
- $this->itip->mail_itip_inline_ui(
- $event,
- $this->ical->method,
- $mime_id.':'.$idx,
- 'calendar',
- rcube_utils::anytodatetime($this->message->headers->date)
- )
- );
+ if ($this->ical->method) {
+ $html .= html::div('calendar-invitebox',
+ $this->itip->mail_itip_inline_ui(
+ $event,
+ $this->ical->method,
+ $mime_id.':'.$idx,
+ 'calendar',
+ rcube_utils::anytodatetime($this->message->headers->date)
+ )
+ );
+ }
// limit listing
if ($idx >= 3)
@@ -2280,7 +2285,7 @@ class calendar extends rcube_plugin
}
// add "Save to calendar" button into attachment menu
- if (!empty($this->ics_parts)) {
+ if ($has_events) {
$this->add_button(array(
'id' => 'attachmentsavecal',
'name' => 'attachmentsavecal',
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 0757c63..62ddea0 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1305,6 +1305,7 @@ class tasklist extends rcube_plugin
// @todo: Calendar plugin does the same, which means the
// attachment body is fetched twice, this is not optimal
$html = '';
+ $has_tasks = false;
foreach ($this->ics_parts as $mime_id) {
$part = $this->message->mime_parts[$mime_id];
$charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
@@ -1322,16 +1323,20 @@ class tasklist extends rcube_plugin
continue;
}
+ $has_tasks = true;
+
// get prepared inline UI for this event object
- $html .= html::div('tasklist-invitebox',
- $this->itip->mail_itip_inline_ui(
- $task,
- $this->ical->method,
- $mime_id . ':' . $idx,
- 'tasks',
- rcube_utils::anytodatetime($this->message->headers->date)
- )
- );
+ if ($this->ical->method) {
+ $html .= html::div('tasklist-invitebox',
+ $this->itip->mail_itip_inline_ui(
+ $task,
+ $this->ical->method,
+ $mime_id . ':' . $idx,
+ 'tasks',
+ rcube_utils::anytodatetime($this->message->headers->date)
+ )
+ );
+ }
// limit listing
if ($idx >= 3) {
@@ -1348,8 +1353,10 @@ class tasklist extends rcube_plugin
$p['content'] = $html . $p['content'];
$this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm');
+ }
- // add "Save to calendar" button into attachment menu
+ // add "Save to tasks" button into attachment menu
+ if ($has_tasks) {
$this->add_button(array(
'id' => 'attachmentsavetask',
'name' => 'attachmentsavetask',
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')),
commit 445edd09b76fc5e029e5f94e70a64a89907ae965
Author: Aleksander Machniak <machniak at kolabsys.com>
Date: Tue Jul 29 15:11:05 2014 +0200
Added iTip handling in tasklist plugin (code copied from Calendar)
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 0b71377..f43fbb9 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -77,7 +77,7 @@ $labels['at'] = 'at';
$labels['this'] = 'this';
$labels['next'] = 'next';
-// mesages
+// messages
$labels['savingdata'] = 'Saving data...';
$labels['errorsaving'] = 'Failed to save data.';
$labels['notasksfound'] = 'No tasks found for the given criteria';
@@ -138,3 +138,17 @@ $labels['errornotifying'] = 'Failed to send notifications to task participants';
$labels['andnmore'] = '$nr more...';
$labels['delegatedto'] = 'Delegated to: ';
$labels['delegatedfrom'] = 'Delegated from: ';
+$labels['savetotasklist'] = 'Save to tasks';
+$labels['comment'] = 'Comment';
+$labels['errorimportingtask'] = 'Failed to import task(s)';
+$labels['importwarningexists'] = 'A copy of this task already exists in your tasklist.';
+$labels['importsuccess'] = 'Successfully imported $nr tasks';
+$labels['newerversionexists'] = 'A newer version of this task already exists! Aborted.';
+$labels['nowritetasklistfound'] = 'No tasklist found to save the task';
+$labels['importedsuccessfully'] = 'The task was successfully added to \'$list\'';
+$labels['updatedsuccessfully'] = 'The task was successfully updated in \'$list\'';
+$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
+$labels['itipresponseerror'] = 'Failed to send the response to this task invitation';
+$labels['itipinvalidrequest'] = 'This invitation is no longer valid';
+$labels['sentresponseto'] = 'Successfully sent invitation response to $mailto';
+$labels['successremoval'] = 'The task has been deleted successfully.';
diff --git a/plugins/tasklist/skins/larry/images/ical-attachment.png b/plugins/tasklist/skins/larry/images/ical-attachment.png
new file mode 100644
index 0000000..8fa486a
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/ical-attachment.png differ
diff --git a/plugins/tasklist/skins/larry/images/loading_blue.gif b/plugins/tasklist/skins/larry/images/loading_blue.gif
new file mode 100644
index 0000000..2ea6b19
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/loading_blue.gif differ
diff --git a/plugins/tasklist/skins/larry/images/tasklist.png b/plugins/tasklist/skins/larry/images/tasklist.png
new file mode 100644
index 0000000..50ed630
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/tasklist.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 7bdfe57..bcdf29a 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -20,7 +20,8 @@
background-position: 0 -26px;
}
-ul.toolbarmenu li span.icon.taskadd {
+ul.toolbarmenu li span.icon.taskadd,
+#attachmentmenu li a.tasklistlink span.icon.taskadd {
background-image: url(buttons.png);
background-position: -4px -90px;
}
@@ -1085,6 +1086,98 @@ label.block {
margin-bottom: 0.5em;
}
+/* Invitation UI in mail */
+
+.messagelist tbody .attachment span.ical {
+ display: inline-block;
+ vertical-align: middle;
+ height: 18px;
+ width: 20px;
+ padding: 0;
+ background: url(images/ical-attachment.png) 2px 1px no-repeat;
+}
+
+div.tasklist-invitebox {
+ min-height: 20px;
+ margin: 5px 8px;
+ padding: 3px 6px 6px 34px;
+ border: 1px solid #ffdf0e;
+ background: url(images/tasklist.png) 6px 5px no-repeat #fef893;
+}
+
+div.tasklist-invitebox td.ititle {
+ font-weight: bold;
+ padding-right: 0.5em;
+}
+
+div.tasklist-invitebox td.label {
+ color: #666;
+ padding-right: 1em;
+}
+
+#event-rsvp .rsvp-buttons,
+div.tasklist-invitebox .itip-buttons div {
+ margin-top: 0.5em;
+}
+
+#event-rsvp input.button,
+div.tasklist-invitebox input.button {
+ font-weight: bold;
+ margin-right: 0.5em;
+}
+
+div.tasklist-invitebox .folder-select {
+ font-weight: 10px;
+ margin-left: 1em;
+}
+
+div.tasklist-invitebox .rsvp-status {
+ padding-left: 2px;
+}
+
+div.tasklist-invitebox .rsvp-status.loading {
+ color: #666;
+ padding: 1px 0 2px 24px;
+ background: url(images/loading_blue.gif) top left no-repeat;
+}
+
+div.tasklist-invitebox .rsvp-status.hint {
+ color: #666;
+ text-shadow: none;
+ font-style: italic;
+}
+
+#event-partstat .changersvp,
+div.tasklist-invitebox .rsvp-status.declined,
+div.tasklist-invitebox .rsvp-status.tentative,
+div.tasklist-invitebox .rsvp-status.accepted,
+div.tasklist-invitebox .rsvp-status.delegated,
+div.tasklist-invitebox .rsvp-status.needs-action {
+ padding: 0 0 1px 22px;
+ background: url(images/attendee-status.png) 2px -20px no-repeat;
+}
+
+#event-partstat .changersvp.declined,
+div.tasklist-invitebox .rsvp-status.declined {
+ background-position: 2px -40px;
+}
+
+#event-partstat .changersvp.tentative,
+div.tasklist-invitebox .rsvp-status.tentative {
+ background-position: 2px -60px;
+}
+
+#event-partstat .changersvp.delegated,
+div.tasklist-invitebox .rsvp-status.delegated {
+ background-position: 2px -180px;
+}
+
+#event-partstat .changersvp.needs-action,
+div.tasklist-invitebox .rsvp-status.needs-action {
+ background-position: 2px 0;
+}
+
+
/** Special hacks for IE7 **/
/** They need to be in this file to also affect the task-create dialog embedded in mail view **/
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 34b1ffb..76ef251 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -56,6 +56,7 @@ class tasklist extends rcube_plugin
private $collapsed_tasks = array();
private $itip;
+ private $ical;
/**
@@ -107,15 +108,19 @@ class tasklist extends rcube_plugin
$this->register_action('mail2task', array($this, 'mail_message2task'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('upload', array($this, 'attachment_upload'));
+ $this->register_action('mailimportitip', array($this, 'mail_import_itip'));
+ $this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
+ $this->register_action('itip-status', array($this, 'task_itip_status'));
+ $this->register_action('itip-remove', array($this, 'task_itip_remove'));
+ $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
$this->add_hook('refresh', array($this, 'refresh'));
$this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
}
else if ($args['task'] == 'mail') {
- // TODO: register hooks to catch ical/vtodo email attachments
if ($args['action'] == 'show' || $args['action'] == 'preview') {
- // $this->add_hook('message_load', array($this, 'mail_message_load'));
- // $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
+ $this->add_hook('message_load', array($this, 'mail_message_load'));
+ $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
@@ -1255,6 +1260,532 @@ class tasklist extends rcube_plugin
$this->rc->output->send();
}
+ /**
+ * Check mail message structure of there are .ics files attached
+ *
+ * @todo move to libcalendaring
+ */
+ public function mail_message_load($p)
+ {
+ $this->message = $p['object'];
+ $itip_part = null;
+
+ // check all message parts for .ics files
+ foreach ((array)$this->message->mime_parts as $part) {
+ if ($this->is_vcalendar($part)) {
+ if ($part->ctype_parameters['method'])
+ $itip_part = $part->mime_id;
+ else
+ $this->ics_parts[] = $part->mime_id;
+ }
+ }
+
+ // priorize part with method parameter
+ if ($itip_part) {
+ $this->ics_parts = array($itip_part);
+ }
+ }
+
+ /**
+ * Add UI element to copy event invitations or updates to the calendar
+ *
+ * @todo move to libcalendaring
+ */
+ public function mail_messagebody_html($p)
+ {
+ // load iCalendar functions (if necessary)
+ if (!empty($this->ics_parts)) {
+ $this->get_ical();
+ $this->load_itip();
+ }
+
+ // @todo: Calendar plugin does the same, which means the
+ // attachment body is fetched twice, this is not optimal
+ $html = '';
+ foreach ($this->ics_parts as $mime_id) {
+ $part = $this->message->mime_parts[$mime_id];
+ $charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
+ $objects = $this->ical->import($this->message->get_part_content($mime_id), $charset);
+ $title = $this->gettext('title');
+
+ // successfully parsed events?
+ if (empty($objects)) {
+ continue;
+ }
+
+ // show a box for every task in the file
+ foreach ($objects as $idx => $task) {
+ if ($task['_type'] != 'task') {
+ continue;
+ }
+
+ // get prepared inline UI for this event object
+ $html .= html::div('tasklist-invitebox',
+ $this->itip->mail_itip_inline_ui(
+ $task,
+ $this->ical->method,
+ $mime_id . ':' . $idx,
+ 'tasks',
+ rcube_utils::anytodatetime($this->message->headers->date)
+ )
+ );
+
+ // limit listing
+ if ($idx >= 3) {
+ break;
+ }
+ }
+ }
+
+ // prepend event boxes to message body
+ if ($html) {
+ $this->load_ui();
+ $this->ui->init();
+
+ $p['content'] = $html . $p['content'];
+
+ $this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm');
+
+ // add "Save to calendar" button into attachment menu
+ $this->add_button(array(
+ 'id' => 'attachmentsavetask',
+ 'name' => 'attachmentsavetask',
+ 'type' => 'link',
+ 'wrapper' => 'li',
+ 'command' => 'attachment-save-task',
+ 'class' => 'icon tasklistlink',
+ 'classact' => 'icon tasklistlink active',
+ 'innerclass' => 'icon taskadd',
+ 'label' => 'tasklist.savetotasklist',
+ ), 'attachmentmenu');
+ }
+
+ return $p;
+ }
+
+ /**
+ * Read the given mime message from IMAP and parse ical data
+ *
+ * @todo move to libcalendaring
+ */
+ private function mail_get_itip_task($mbox, $uid, $mime_id)
+ {
+ $charset = RCMAIL_CHARSET;
+
+ // establish imap connection
+ $imap = $this->rc->get_storage();
+ $imap->set_mailbox($mbox);
+
+ if ($uid && $mime_id) {
+ list($mime_id, $index) = explode(':', $mime_id);
+
+ $part = $imap->get_message_part($uid, $mime_id);
+ $headers = $imap->get_message_headers($uid);
+
+ if ($part->ctype_parameters['charset']) {
+ $charset = $part->ctype_parameters['charset'];
+ }
+
+ if ($part) {
+ $tasks = $this->get_ical()->import($part, $charset);
+ }
+ }
+
+ // successfully parsed events?
+ if (!empty($tasks) && ($task = $tasks[$index])) {
+ // 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']);
+
+ return $task;
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks if specified message part is a vcalendar data
+ *
+ * @param rcube_message_part Part object
+ *
+ * @return boolean True if part is of type vcard
+ *
+ * @todo move to libcalendaring
+ */
+ private function is_vcalendar($part)
+ {
+ return (
+ in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
+ // Apple sends files as application/x-any (!?)
+ ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
+ );
+ }
+
+ /**
+ * Load iCalendar functions
+ */
+ public function get_ical()
+ {
+ if (!$this->ical) {
+ $this->ical = libcalendaring::get_ical();
+ }
+
+ return $this->ical;
+ }
+
+ /**
+ * Get properties of the tasklist this user has specified as default
+ */
+ public function get_default_tasklist($writeable = false)
+ {
+// $default_id = $this->rc->config->get('tasklist_default_list');
+ $lists = $this->driver->get_lists();
+// $list = $calendars[$default_id] ?: null;
+
+ if (!$list || ($writeable && !$list['editable'])) {
+ foreach ($lists as $l) {
+ if ($l['default']) {
+ $list = $l;
+ break;
+ }
+
+ if (!$writeable || $l['editable']) {
+ $first = $l;
+ }
+ }
+ }
+
+ return $list ?: $first;
+ }
+
+ /**
+ * Import the full payload from a mail message attachment
+ */
+ public function mail_import_attachment()
+ {
+ $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
+ $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
+ $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
+ $charset = RCMAIL_CHARSET;
+
+ // establish imap connection
+ $imap = $this->rc->get_storage();
+ $imap->set_mailbox($mbox);
+
+ if ($uid && $mime_id) {
+ $part = $imap->get_message_part($uid, $mime_id);
+ $headers = $imap->get_message_headers($uid);
+
+ if ($part->ctype_parameters['charset']) {
+ $charset = $part->ctype_parameters['charset'];
+ }
+
+ if ($part) {
+ $tasks = $this->get_ical()->import($part, $charset);
+ }
+ }
+
+ $success = $existing = 0;
+
+ if (!empty($tasks)) {
+ // find writeable tasklist to store task
+ $cal_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
+ $lists = $this->driver->get_lists();
+ $list = $lists[$cal_id] ?: $this->get_default_tasklist(true);
+
+ foreach ($tasks as $task) {
+ // save to tasklist
+ if ($list && $list['editable'] && $task['_type'] == 'task') {
+ $task['list'] = $list['id'];
+
+ if (!$this->driver->get_task($task['uid'])) {
+ $success += (bool) $this->driver->create_task($task);
+ }
+ else {
+ $existing++;
+ }
+ }
+ }
+ }
+
+ if ($success) {
+ $this->rc->output->command('display_message', $this->gettext(array(
+ 'name' => 'importsuccess',
+ 'vars' => array('nr' => $success),
+ )), 'confirmation');
+ }
+ else if ($existing) {
+ $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('errorimportingtask'), 'error');
+ }
+ }
+
+ /**
+ * Handler for POST request to import an event attached to a mail message
+ */
+ public function mail_import_itip()
+ {
+ $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
+ $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
+ $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
+ $status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
+ $delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
+ $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)) || $status == 'needs-action';
+
+ $error_msg = $this->gettext('errorimportingtask');
+ $success = false;
+
+ // successfully parsed tasks?
+ if ($task = $this->mail_get_itip_task($mbox, $uid, $mime_id)) {
+ // find writeable list to store the task
+ $list_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
+ $lists = $this->driver->get_lists();
+ $list = $lists[$list_id] ?: $this->get_default_tasklist(true);
+
+ $metadata = array(
+ 'uid' => $task['uid'],
+ 'changed' => is_object($task['changed']) ? $task['changed']->format('U') : 0,
+ 'sequence' => intval($task['sequence']),
+ 'fallback' => strtoupper($status),
+ 'method' => $this->ical->method,
+ 'task' => 'tasks',
+ );
+
+ // update my attendee status according to submitted method
+ if (!empty($status)) {
+ $organizer = null;
+ $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)) {
+ $metadata['attendee'] = $attendee['email'];
+ $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
+ $reply_sender = $attendee['email'];
+
+ $task['attendees'][$i]['status'] = strtoupper($status);
+ if ($task['attendees'][$i]['status'] != 'NEEDS-ACTION') {
+ unset($task['attendees'][$i]['rsvp']); // remove RSVP attribute
+ }
+ }
+ }
+
+ // add attendee with this user's default identity if not listed
+ if (!$reply_sender) {
+ $sender_identity = $this->rc->user->get_identity();
+ $task['attendees'][] = array(
+ 'name' => $sender_identity['name'],
+ 'email' => $sender_identity['email'],
+ 'role' => 'OPT-PARTICIPANT',
+ 'status' => strtoupper($status),
+ );
+ $metadata['attendee'] = $sender_identity['email'];
+ }
+ }
+
+ // save to tasklist
+ if ($list && $list['editable']) {
+ $task['list'] = $list['id'];
+
+ // check for existing task with the same UID
+ $existing = $this->driver->get_task($task['uid']);
+
+ if ($existing) {
+ // only update attendee status
+ if ($this->ical->method == 'REPLY') {
+ // try to identify the attendee using the email sender address
+ $existing_attendee = -1;
+ foreach ($existing['attendees'] as $i => $attendee) {
+ if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
+ $existing_attendee = $i;
+ break;
+ }
+ }
+
+ $task_attendee = null;
+ foreach ($task['attendees'] as $attendee) {
+ if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
+ $task_attendee = $attendee;
+ $metadata['fallback'] = $attendee['status'];
+ $metadata['attendee'] = $attendee['email'];
+ $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
+ break;
+ }
+ }
+
+ // found matching attendee entry in both existing and new events
+ if ($existing_attendee >= 0 && $task_attendee) {
+ $existing['attendees'][$existing_attendee] = $task_attendee;
+ $success = $this->driver->edit_task($existing);
+ }
+ // update the entire attendees block
+ else if (($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) && $task_attendee) {
+ $existing['attendees'][] = $task_attendee;
+ $success = $this->driver->edit_task($existing);
+ }
+ else {
+ $error_msg = $this->gettext('newerversionexists');
+ }
+ }
+ // delete the task when declined
+ else if ($status == 'declined' && $delete) {
+ $deleted = $this->driver->delete_task($existing, true);
+ $success = true;
+ }
+ // import the (newer) task
+ else if ($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) {
+ $task['id'] = $existing['id'];
+ $task['list'] = $existing['list'];
+
+ // preserve my participant status for regular updates
+ if (empty($status)) {
+ $emails = $this->lib->get_user_emails();
+ foreach ($task['attendees'] as $i => $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ foreach ($existing['attendees'] as $j => $_attendee) {
+ if ($attendee['email'] == $_attendee['email']) {
+ $task['attendees'][$i] = $existing['attendees'][$j];
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // set status=CANCELLED on CANCEL messages
+ if ($this->ical->method == 'CANCEL') {
+ $task['status'] = 'CANCELLED';
+ }
+ // show me as free when declined (#1670)
+ if ($status == 'declined' || $task['status'] == 'CANCELLED') {
+ $task['free_busy'] = 'free';
+ }
+
+ $success = $this->driver->edit_task($task);
+ }
+ else if (!empty($status)) {
+ $existing['attendees'] = $task['attendees'];
+ if ($status == 'declined') { // show me as free when declined (#1670)
+ $existing['free_busy'] = 'free';
+ }
+
+ $success = $this->driver->edit_event($existing);
+ }
+ else {
+ $error_msg = $this->gettext('newerversionexists');
+ }
+ }
+ else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_tasklists'))) {
+ $success = $this->driver->create_task($task);
+ }
+ else if ($status == 'declined') {
+ $error_msg = null;
+ }
+ }
+ else if ($status == 'declined') {
+ $error_msg = null;
+ }
+ else {
+ $error_msg = $this->gettext('nowritetasklistfound');
+ }
+ }
+
+ if ($success) {
+ $message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
+ $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
+
+ $metadata['rsvp'] = intval($metadata['rsvp']);
+ $metadata['after_action'] = $this->rc->config->get('tasklist_itip_after_action');
+
+ $this->rc->output->command('plugin.itip_message_processed', $metadata);
+ $error_msg = null;
+ }
+ else if ($error_msg) {
+ $this->rc->output->command('display_message', $error_msg, 'error');
+ }
+
+ // send iTip reply
+ if ($this->ical->method == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
+ $task['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+ $itip = $this->load_itip();
+ $itip->set_sender_email($reply_sender);
+
+ if ($itip->send_itip_message($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');
+ }
+
+ $this->rc->output->send();
+ }
+
+
+ /**** Task invitation plugin hooks ****/
+
+ /**
+ * Handler for calendar/itip-status requests
+ */
+ function task_itip_status()
+ {
+ $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
+
+ // find local copy of the referenced task
+ $existing = $this->driver->get_task($data);
+ $itip = $this->load_itip();
+ $response = $itip->get_itip_status($data, $existing);
+
+ // get a list of writeable lists to save new tasks to
+ if (!$existing && $response['action'] == 'rsvp' || $response['action'] == 'import') {
+ $lists = $this->driver->get_lists();
+ $select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true));
+ $num = 0;
+
+ foreach ($lists as $list) {
+ if ($list['editable']) {
+ $select->add($list['name'], $list['id']);
+ $num++;
+ }
+ }
+
+ if ($num <= 1) {
+ $select = null;
+ }
+ }
+
+ if ($select) {
+ $default_list = $this->get_default_tasklist(true);
+ $response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . ' ' .
+ $select->show($this->rc->config->get('tasklist_default_list', $default_list['id'])));
+ }
+
+ $this->rc->output->command('plugin.update_itip_object_status', $response);
+ }
+
+ /**
+ * Handler for calendar/itip-remove requests
+ */
+ function task_itip_remove()
+ {
+ $success = false;
+ $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
+
+ // search for event if only UID is given
+ if ($task = $this->driver->get_task($uid)) {
+ $success = $this->driver->delete_task($task, true);
+ }
+
+ if ($success) {
+ $this->rc->output->show_message('tasklist.successremoval', 'confirmation');
+ }
+ else {
+ $this->rc->output->show_message('tasklist.errorsaving', 'error');
+ }
+ }
+
/******* Utility functions *******/
@@ -1275,4 +1806,3 @@ class tasklist extends rcube_plugin
return $this->driver->user_delete($args);
}
}
-
diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js
index 6e93b15..81e27f0 100644
--- a/plugins/tasklist/tasklist_base.js
+++ b/plugins/tasklist/tasklist_base.js
@@ -37,6 +37,7 @@ function rcube_tasklist(settings)
/* public methods */
this.create_from_mail = create_from_mail;
this.mail2taskdialog = mail2task_dialog;
+ this.save_to_tasklist = save_to_tasklist;
/**
@@ -80,21 +81,46 @@ function rcube_tasklist(settings)
this.ui.edit_task(null, 'new', prop);
}
+ // handler for attachment-save-tasklist commands
+ function save_to_tasklist()
+ {
+ // TODO: show dialog to select the tasklist for importing
+ if (this.selected_attachment && window.rcube_libcalendaring) {
+ rcmail.http_post('tasks/mailimportattach', {
+ _uid: rcmail.env.uid,
+ _mbox: rcmail.env.mailbox,
+ _part: this.selected_attachment,
+ // _list: $('#tasklist-attachment-saveto').val(),
+ }, rcmail.set_busy(true, 'itip.savingdata'));
+ }
+ }
+
}
/* tasklist plugin initialization (for email task) */
window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', function(evt) {
var tasks = new rcube_tasklist(rcmail.env.libcal_settings);
- rcmail.register_command('tasklist-create-from-mail', function() { tasks.create_from_mail() });
- rcmail.addEventListener('plugin.mail2taskdialog', function(p){ tasks.mail2taskdialog(p) });
- rcmail.addEventListener('plugin.unlock_saving', function(p){ tasks.ui && tasks.ui.unlock_saving(); });
+ rcmail.register_command('tasklist-create-from-mail', function() { tasks.create_from_mail(); });
+ rcmail.register_command('attachment-save-task', function() { tasks.save_to_tasklist(); });
+ rcmail.addEventListener('plugin.mail2taskdialog', function(p) { tasks.mail2taskdialog(p); });
+ rcmail.addEventListener('plugin.unlock_saving', function(p) { tasks.ui && tasks.ui.unlock_saving(); });
if (rcmail.env.action != 'show')
rcmail.env.message_commands.push('tasklist-create-from-mail');
else
rcmail.enable_command('tasklist-create-from-mail', true);
+ rcmail.addEventListener('beforemenu-open', function(p) {
+ if (p.menu == 'attachmentmenu') {
+ tasks.selected_attachment = p.id;
+ var mimetype = rcmail.env.attachments[p.id],
+ is_ics = mimetype == 'text/calendar' || mimetype == 'text/x-vcalendar' || mimetype == 'application/ics';
+
+ rcmail.enable_command('attachment-save-task', is_ics);
+ }
+ });
+
// add contextmenu item
if (window.rcm_contextmenu_register_command) {
rcm_contextmenu_register_command(
@@ -104,4 +130,3 @@ window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', fu
'moveto');
}
});
-
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);
commit c0da6448fb6f1a0955f909f9bbca0d5fd80133d4
Author: Aleksander Machniak <machniak at kolabsys.com>
Date: Wed Jul 23 14:35:52 2014 +0200
Start implementing Assignments (Attendees) tab for tasks (#1165)
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 9fd0e3e..7920c94 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -58,6 +58,7 @@ $labels['taskactions'] = 'Task options...';
$labels['tabsummary'] = 'Summary';
$labels['tabrecurrence'] = 'Recurrence';
+$labels['tabassignments'] = 'Assignments';
$labels['tabattachments'] = 'Attachments';
$labels['tabsharing'] = 'Sharing';
@@ -96,3 +97,38 @@ $labels['arialabellistsearchform'] = 'Tasklists search form';
$labels['arialabeltaskselector'] = 'List mode';
$labels['arialabeltasklisting'] = 'Tasks listing';
+// attendees
+$labels['attendee'] = 'Participant';
+$labels['role'] = 'Role';
+$labels['availability'] = 'Avail.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Add participant';
+$labels['roleorganizer'] = 'Organizer';
+$labels['rolerequired'] = 'Required';
+$labels['roleoptional'] = 'Optional';
+$labels['rolechair'] = 'Chair';
+$labels['rolenonparticipant'] = 'Absent';
+$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.";
+
+// 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['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?';
+$labels['itipcomment'] = 'Invitation/notification comment';
+$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants';
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index e93a311..1e847b3 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -849,6 +849,84 @@ a.morelink:hover {
border-bottom: 2px solid #fafafa;
}
+.edit-attendees-table {
+ width: 100%;
+ margin-top: 0.5em;
+}
+
+.edit-attendees-table th.role,
+.edit-attendees-table td.role {
+ width: 9em;
+}
+
+.edit-attendees-table th.availability,
+.edit-attendees-table td.availability,
+.edit-attendees-table th.confirmstate,
+.edit-attendees-table td.confirmstate {
+ width: 4em;
+}
+
+.edit-attendees-table th.options,
+.edit-attendees-table td.options {
+ width: 16px;
+ padding: 2px 4px;
+}
+
+.edit-attendees-table th.sendmail,
+.edit-attendees-table td.sendmail {
+ width: 44px;
+ padding: 2px;
+}
+
+.edit-attendees-table th.sendmail label {
+ display: inline-block;
+ position: relative;
+ top: 4px;
+ width: 24px;
+ height: 18px;
+ min-width: 24px;
+ padding: 0;
+ overflow: hidden;
+ text-indent: -5000px;
+ white-space: nowrap;
+ background: url(images/sendinvitation.png) 1px 0 no-repeat;
+}
+
+.edit-attendees-table th.name,
+.edit-attendees-table td.name {
+ width: auto;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.edit-attendees-table td.name select {
+ width: 100%;
+}
+
+.edit-attendees-table a.deletelink {
+ display: inline-block;
+ width: 17px;
+ height: 17px;
+ padding: 0;
+ overflow: hidden;
+ text-indent: 1000px;
+}
+
+#edit-attendees-form {
+ position: relative;
+ margin-top: 15px;
+}
+
+#edit-attendees-form .attendees-invitebox {
+ text-align: right;
+ margin: 0;
+}
+
+#edit-attendees-form .attendees-invitebox label {
+ padding-right: 3px;
+}
+
#taskedit-attachments {
margin: 0.6em 0;
}
@@ -954,4 +1032,3 @@ label.block {
html.ie7 #taskedit-completeness-slider {
display: inline;
}
-
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index c4b3c13..599974e 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -1,7 +1,7 @@
<div id="taskedit" class="uidialog uidialog-tabbed">
<form id="taskeditform" action="#" method="post" enctype="multipart/form-data">
<ul>
- <li><a href="#taskedit-panel-main"><roundcube:label name="tasklist.tabsummary" /></a></li><li><a href="#taskedit-panel-recurrence"><roundcube:label name="tasklist.tabrecurrence" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-panel-attachments"><roundcube:label name="tasklist.tabattachments" /></a></li>
+ <li><a href="#taskedit-panel-main"><roundcube:label name="tasklist.tabsummary" /></a></li><li><a href="#taskedit-panel-recurrence"><roundcube:label name="tasklist.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#taskedit-panel-attendees"><roundcube:label name="tasklist.tabassignments" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-panel-attachments"><roundcube:label name="tasklist.tabattachments" /></a></li>
</ul>
<!-- basic info -->
<div id="taskedit-panel-main">
@@ -79,6 +79,13 @@
<roundcube:object name="plugin.recurrence_form" part="rdate" class="form-section" />
</div>
</div>
+ <!-- attendees list (assignments) -->
+ <div id="taskedit-panel-attendees">
+ <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" />
+ <roundcube:include file="/templates/freebusylegend.html" />
+ </div>
<!-- attachments list (with upload form) -->
<div id="taskedit-panel-attachments">
<div id="taskedit-attachments">
@@ -91,4 +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" />
</div>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 0852e12..b0c95ba 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -82,6 +82,9 @@ function rcube_tasklist_ui(settings)
var tasklists_widget;
var focused_task;
var focused_subclass;
+ var task_attendees = [];
+ var attendees_list;
+// var resources_list;
var me = this;
// general datepicker settings
@@ -541,6 +544,34 @@ function rcube_tasklist_ui(settings)
if (sel) $(sel).val('');
return false;
});
+
+ // init attendees autocompletion
+ var ac_props;
+ // parallel autocompletion
+ if (rcmail.env.autocomplete_threads > 0) {
+ ac_props = {
+ threads: rcmail.env.autocomplete_threads,
+ sources: rcmail.env.autocomplete_sources
+ };
+ }
+ rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
+ rcmail.addEventListener('autocomplete_insert', function(e){
+ if (e.field.name == 'participant') {
+ $('#edit-attendee-add').click();
+ }
+// else if (e.field.name == 'resource' && e.data && e.data.email) {
+// add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
+// e.field.value = '';
+// }
+ });
+
+ $('#edit-attendee-add').click(function(){
+ var input = $('#edit-attendee-name');
+ rcmail.ksearch_blur();
+ if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
+ input.val('');
+ }
+ });
}
/**
@@ -1298,6 +1329,177 @@ function rcube_tasklist_ui(settings)
scroll_timer = window.setTimeout(function(){ tasklist_drag_scroll(container, dir); }, scroll_speed);
}
+ // check if the task has 'real' attendees, excluding the current user
+ var has_attendees = function(task)
+ {
+ return !!(task.attendees && task.attendees.length && (task.attendees.length > 1 || String(task.attendees[0].email).toLowerCase() != settings.identity.email));
+ };
+
+ // 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;
+
+ 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];
+ }
+
+ return false;
+ };
+
+ // check if the current user is the organizer
+ var is_organizer = function(task, email)
+ {
+ return is_attendee(task, 'ORGANIZER', email) || !task.id;
+ };
+
+ // 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'));
+ }
+ }
+
+ return success;
+ };
+
+ // add the given attendee to the list
+ var add_attendee = function(data, readonly)
+ {
+ 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;
+
+ 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']();
+ });
+
+ // 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;
+ };
+
+ // 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.calendar_resources && cutype == 'RESOURCE') {
+ event_resources_dialog(mailto);
+ }
+ else {
+ rcmail.redirect(rcmail.url('mail/compose', { _to:mailto }));
+ }
+ return false;
+ };
+
+ // remove an attendee from the list
+ var remove_attendee = function(elem, id)
+ {
+ $(elem).closest('tr').remove();
+ task_attendees = $.grep(task_attendees, function(data) { return (data.name != id && data.email != id) });
+ };
+
/**
* Show task details in a dialog
*/
@@ -1422,7 +1624,7 @@ function rcube_tasklist_ui(settings)
return false;
me.selected_task = $.extend({ valarms:[] }, rec); // clone task object
- rec = me.selected_task;
+ rec = me.selected_task;
// assign temporary id
if (!me.selected_task.id)
@@ -1442,6 +1644,12 @@ function rcube_tasklist_ui(settings)
completeness_slider.slider('value', complete.val());
var taskstatus = $('#taskedit-status').val(rec.status || '');
var tasklist = $('#taskedit-tasklist').val(rec.list || me.selected_list).prop('disabled', rec.parent_id ? true : false);
+ var notify = $('#edit-attendees-donotify').get(0);
+ var invite = $('#edit-attendees-invite').get(0);
+ var comment = $('#edit-attendees-comment');
+
+ notify.checked = has_attendees(rec);
+ invite.checked = true;
// tag-edit line
var tagline = $(rcmail.gui_objects.edittagline).empty();
@@ -1468,6 +1676,49 @@ function rcube_tasklist_ui(settings)
// set recurrence
me.set_recurrence_edit(rec);
+ // init attendees tab
+ var organizer = !rec.attendees || is_organizer(rec),
+ allow_invitations = organizer || (rec.owner && rec.owner == 'anonymous') || settings.invite_shared;
+
+ 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()
+ {
+ 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) {
+ reply_selected++;
+ }
+ }
+ }
+
+ // make sure comment box is visible if at least one attendee has reply enabled
+ // or global "send invitations" checkbox is checked
+ if (reply_selected || $('#edit-attendees-invite:checked').length) {
+ $('p.attendees-commentbox').show();
+ }
+
+ // select the correct organizer identity
+ var identity_id = 0;
+ $.each(settings.identities, function(i,v) {
+ if (organizer && v == 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')]();
+ };
+
// attachments
rcmail.enable_command('remove-attachment', list.editable);
me.selected_task.deleted_attachments = [];
@@ -1491,23 +1742,26 @@ function rcube_tasklist_ui(settings)
// define dialog buttons
var buttons = {};
buttons[rcmail.gettext('save', 'tasklist')] = function() {
+ var data = me.selected_task;
+
// copy form field contents into task object to save
$.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus, list:tasklist }, function(key,input){
- me.selected_task[key] = input.val();
+ data[key] = input.val();
});
- me.selected_task.tags = [];
- me.selected_task.attachments = [];
- me.selected_task.valarms = me.serialize_alarms('#taskedit-alarms');
- me.selected_task.recurrence = me.serialize_recurrence(rectime.val());
+ data.tags = [];
+ data.attachments = [];
+ data.attendees = task_attendees;
+ data.valarms = me.serialize_alarms('#taskedit-alarms');
+ data.recurrence = me.serialize_recurrence(rectime.val());
// do some basic input validation
- if (!me.selected_task.title || !me.selected_task.title.length) {
+ if (!data.title || !data.title.length) {
title.focus();
return false;
}
- else if (me.selected_task.startdate && me.selected_task.date) {
- var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.startdate, datepicker_settings);
- var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.date, datepicker_settings);
+ else if (data.startdate && data.date) {
+ var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, data.startdate, datepicker_settings);
+ var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, data.date, datepicker_settings);
if (startdate > duedate) {
alert(rcmail.gettext('invalidstartduedates', 'tasklist'));
return false;
@@ -1515,38 +1769,76 @@ function rcube_tasklist_ui(settings)
}
// collect tags
- $('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem){
+ $('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem) {
if (elem.value)
- me.selected_task.tags.push(elem.value);
+ data.tags.push(elem.value);
});
// including the "pending" one in the text box
var newtag = $('#tagedit-input').val();
if (newtag != '') {
- me.selected_task.tags.push(newtag);
+ data.tags.push(newtag);
}
// uploaded attachments list
for (var i in rcmail.env.attachments) {
if (i.match(/^rcmfile(.+)/))
- me.selected_task.attachments.push(RegExp.$1);
+ data.attachments.push(RegExp.$1);
}
// task assigned to a new list
- if (me.selected_task.list && listdata[id] && me.selected_task.list != listdata[id].list) {
- me.selected_task._fromlist = list.id;
+ if (data.list && listdata[id] && data.list != listdata[id].list) {
+ data._fromlist = list.id;
+ }
+
+ data.complete = complete.val() / 100;
+ if (isNaN(data.complete))
+ data.complete = null;
+
+ if (!data.list && list.id)
+ data.list = list.id;
+
+ 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();
}
- me.selected_task.complete = complete.val() / 100;
- if (isNaN(me.selected_task.complete))
- me.selected_task.complete = null;
+ // don't submit attendees if only myself is added as organizer
+ if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && String(data.attendees[0].email).toLowerCase() == settings.identity.email) {
+ data.attendees = [];
+ }
- if (!me.selected_task.list && list.id)
- me.selected_task.list = list.id;
+ // per-attendee notification suppression
+ var need_invitation = false;
+ if (allow_invitations) {
+ $.each(data.attendees, function (i, v) {
+ if (v.role != 'ORGANIZER') {
+ if ($('input.edit-attendee-reply[value="' + v.email + '"]').prop('checked')) {
+ need_invitation = true;
+ delete data.attendees[i]['noreply'];
+ }
+ else {
+ data.attendees[i].noreply = 1;
+ }
+ }
+ });
+ }
- if (!me.selected_task.tags.length)
- me.selected_task.tags = '';
+ // tell server to send notifications
+ if ((data.attendees.length || (rec.id && rec.attendees.length)) && allow_invitations && (notify.checked || invite.checked || need_invitation)) {
+ data._notify = 1;
+ data._comment = comment.val();
+ }
- if (save_task(me.selected_task, action))
+ if (save_task(data, action))
$dialog.dialog('close');
};
@@ -1581,8 +1873,10 @@ function rcube_tasklist_ui(settings)
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 580);
- }
+ if (tasklist.attendees)
+ window.setTimeout(load_attendees_tab, 1);
+ }
/**
* Open a task attachment either in a browser window for inline view or download it
@@ -2060,6 +2354,29 @@ function rcube_tasklist_ui(settings)
/**** Utility functions ****/
+ // same as str.split(delimiter) but it ignores delimiters within quoted strings
+ var explode_quoted_string = function(str, delimiter)
+ {
+ var result = [],
+ strlen = str.length,
+ q, p, i, char, last;
+
+ for (q = p = i = 0; i < strlen; i++) {
+ char = str.charAt(i);
+ if (char == '"' && last != '\\') {
+ q = !q;
+ }
+ else if (!q && char == delimiter) {
+ result.push(str.substring(p, i));
+ p = i + 1;
+ }
+ last = char;
+ }
+
+ result.push(str.substr(p));
+ return result;
+ };
+
/**
* Clear any text selection
* (text is probably selected when double-clicking somewhere)
@@ -2205,7 +2522,7 @@ jQuery.unqiqueStrings = (function() {
var rctasks;
window.rcmail && rcmail.addEventListener('init', function(evt) {
- rctasks = new rcube_tasklist_ui(rcmail.env.libcal_settings);
+ rctasks = new rcube_tasklist_ui($.extend(rcmail.env.tasklist_settings, rcmail.env.libcal_settings));
// register button commands
rcmail.register_command('newtask', function(){ rctasks.edit_task(null, 'new', {}); }, true);
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index fc7320e..4dd7b82 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -55,10 +55,41 @@ class tasklist_ui
$this->plugin->include_script('tasklist_base.js');
// copy config to client
- // $this->rc->output->set_env('tasklist_settings', $settings);
+ $this->rc->output->set_env('tasklist_settings', $this->load_settings());
+
+ // initialize attendees autocompletion
+ $this->rc->autocomplete_init();
$this->ready = true;
- }
+ }
+
+ /**
+ *
+ */
+ function load_settings()
+ {
+ $settings = array();
+
+ //$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
+
+ // get user identity to create default attendee
+ foreach ($this->rc->user->list_identities() as $rec) {
+ if (!$identity)
+ $identity = $rec;
+
+ $identity['emails'][] = $rec['email'];
+ $settings['identities'][$rec['identity_id']] = $rec['email'];
+ }
+
+ $identity['emails'][] = $this->rc->user->get_username();
+ $settings['identity'] = array(
+ 'name' => $identity['name'],
+ 'email' => strtolower($identity['email']),
+ 'emails' => ';' . strtolower(join(';', $identity['emails']))
+ );
+
+ return $settings;
+ }
/**
* Register handler methods for the template engine
@@ -78,6 +109,9 @@ class tasklist_ui
$this->plugin->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
$this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
$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.edit_attendees_notify', array($this, 'edit_attendees_notify'));
$this->plugin->include_script('jquery.tagedit.js');
$this->plugin->include_script('tasklist.js');
@@ -375,4 +409,54 @@ class tasklist_ui
}
}
+ /**
+ *
+ */
+ function attendees_list($attrib = array())
+ {
+ // add "noreply" checkbox to attendees table only
+ $invitations = strpos($attrib['id'], 'attend') !== false;
+
+ $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('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')),
+ $invite->show(1) . html::label('edit-attendees-invite', $this->plugin->gettext('sendinvitations')));
+ }
+ $table->add_header('options', '');
+
+ return $table->show($attrib);
+ }
+
+ /**
+ *
+ */
+ function attendees_form($attrib = array())
+ {
+ $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30));
+ $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment',
+ 'rows' => 4, 'cols' => 55, 'title' => $this->plugin->gettext('itipcommenttitle')));
+
+ return html::div($attrib,
+ html::div(null, $input->show() . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->plugin->gettext('addattendee')))
+ // . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->plugin->gettext('scheduletime').'...'))
+ ) .
+ html::p('attendees-commentbox', html::label(null, $this->plugin->gettext('itipcomment') . $textarea->show()))
+ );
+ }
+
+ /**
+ *
+ */
+ function edit_attendees_notify($attrib = array())
+ {
+ $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1));
+ return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->plugin->gettext('sendnotifications')));
+ }
}
More information about the commits
mailing list