Branch 'dev/task-attendees' - 5 commits - plugins/libcalendaring plugins/libkolab plugins/tasklist
Thomas Brüderli
bruederli at kolabsys.com
Thu Jul 31 12:49:33 CEST 2014
dev/null |binary
plugins/libcalendaring/lib/libcalendaring_itip.php | 15 -
plugins/libcalendaring/localization/en_US.inc | 8
plugins/libkolab/lib/kolab_format_event.php | 2
plugins/libkolab/lib/kolab_format_task.php | 2
plugins/libkolab/lib/kolab_format_xcal.php | 5
plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php | 8
plugins/tasklist/localization/en_US.inc | 32 +-
plugins/tasklist/skins/larry/images/attendee-status.png |binary
plugins/tasklist/skins/larry/images/badge_cancelled.png |binary
plugins/tasklist/skins/larry/tasklist.css | 53 +++-
plugins/tasklist/tasklist.js | 182 +++++++++++----
plugins/tasklist/tasklist.php | 72 +++++
plugins/tasklist/tasklist_base.js | 2
14 files changed, 304 insertions(+), 77 deletions(-)
New commits:
commit f5e93184e3ef08d2e4f9d57f3ade9a41390fe610
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 12:49:27 2014 +0200
Fix iTip replies to organizer (don't increment sequence if I'm not the owner)
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index ef8f31d..bcc5c06 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -902,9 +902,15 @@ class tasklist_kolab_driver extends tasklist_driver
unset($object['attachments']);
}
- $object['_owner'] = $identity['email'];
+ // allow sequence increments if I'm the organizer
+ if ($this->plugin->is_organizer($object)) {
+ unset($object['sequence']);
+ }
+ else if (isset($old['sequence'])) {
+ $object['sequence'] = $old['sequence'];
+ }
- unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['sequence']);
+ unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
return $object;
}
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 3953977..96b5e82 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -949,10 +949,11 @@ a.morelink:hover {
display: block;
color: #333;
font-weight: bold;
- padding: 8px 4px 3px 30px;
+ padding: 3px 4px 3px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
+ line-height: 20px;
}
#taskedit-attachments ul li a.file {
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 9da7a88..a0ff22f 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -992,16 +992,14 @@ function rcube_tasklist_ui(settings)
notify = false, partstat = false, html = '';
// task has attendees, ask whether to notify them
- if (has_attendees(rec)) {
- if (is_organizer(rec)) {
- notify = true;
- html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
- }
- // ask whether to change my partstat and notify organizer
- else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
- partstat = true;
- html = rcmail.gettext('partstatupdatenotification', 'tasklist');
- }
+ if (has_attendees(rec) && is_organizer(rec)) {
+ notify = true;
+ html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
+ }
+ // ask whether to change my partstat and notify organizer
+ else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
+ partstat = true;
+ html = rcmail.gettext('partstatupdatenotification', 'tasklist');
}
// remove to avoid endless recursion
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index ae76f99..728c4a3 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1097,6 +1097,16 @@ class tasklist extends rcube_plugin
return false;
}
+ /**
+ * Determine whether the current user is the organizer of the given task
+ */
+ public function is_organizer($task)
+ {
+ $emails = $this->lib->get_user_emails();
+ return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
+ }
+
+
/******* UI functions ********/
/**
commit f8b6706074b394244b52d479e90dacd99f6c6cce
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 12:43:55 2014 +0200
Fix 'create task from mail' function
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 9ac70bc..ae76f99 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1128,11 +1128,17 @@ class tasklist extends rcube_plugin
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
+ // collect env variables
+ $env = array(
+ 'tasklists' => array(),
+ 'tasklist_settings' => $this->ui->load_settings(),
+ );
+
$this->ui->init_templates();
echo $this->api->output->parse('tasklist.taskedit', false, false);
echo html::tag('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => $this->url($this->local_skin_path() . '/tagedit.css'), 'nl' => true));
echo html::tag('script', array('type' => 'text/javascript'),
- "rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
+ "rcmail.set_env(" . json_encode($env) . ");\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js
index 81e27f0..5448418 100644
--- a/plugins/tasklist/tasklist_base.js
+++ b/plugins/tasklist/tasklist_base.js
@@ -57,7 +57,7 @@ function rcube_tasklist(settings)
// rcmail.gui_object('attachmentlist', 'attachmentlist');
ui_loaded = true;
- me.ui = new rcube_tasklist_ui(settings);
+ me.ui = new rcube_tasklist_ui($.extend(rcmail.env.tasklist_settings, settings));
create_from_mail(uid); // start over
});
return;
commit 0d8b6912ae2864113a520b5f81a0d63447ecf8ff
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 12:22:07 2014 +0200
Fix auto-incrementing the sequence value of xcal objects
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 596d0da..c233f44 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -26,7 +26,7 @@ class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
- public static $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+ public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
protected $objclass = 'Event';
protected $read_func = 'readEvent';
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index ee0ca6a..52744d4 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -26,7 +26,7 @@ class kolab_format_task extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.task';
- public static $scheduling_properties = array('start', 'due', 'summary', 'status');
+ public $scheduling_properties = array('start', 'due', 'summary', 'status');
protected $objclass = 'Todo';
protected $read_func = 'readTodo';
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 6624b02..7d077b7 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -29,7 +29,8 @@ abstract class kolab_format_xcal extends kolab_format
public $CTYPE = 'application/calendar+xml';
public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
- public static $scheduling_properties = array('start', 'end', 'location');
+
+ public $scheduling_properties = array('start', 'end', 'location');
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
@@ -315,7 +316,7 @@ abstract class kolab_format_xcal extends kolab_format
// increment sequence when updating properties relevant for scheduling.
// RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
// TODO: make the list of properties considered 'significant' for scheduling configurable
- foreach (self::$scheduling_properties as $prop) {
+ foreach ($this->scheduling_properties as $prop) {
$a = $old[$prop];
$b = $object[$prop];
if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 6884083..ef8f31d 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -904,7 +904,7 @@ class tasklist_kolab_driver extends tasklist_driver
$object['_owner'] = $identity['email'];
- unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
+ unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['sequence']);
return $object;
}
commit 457195102e070c098899da7972b2fe6a25f572aa
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 11:40:39 2014 +0200
Complete iTip communication on task status changes: ask to notify the organizer on update or deletion + add icons for task-specific partstats and cancelled tasks
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index b37faa8..a7ec1ea 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -51,6 +51,7 @@ $labels['newtask'] = 'New Task';
$labels['edittask'] = 'Edit Task';
$labels['save'] = 'Save';
$labels['cancel'] = 'Cancel';
+$labels['saveandnotify'] = 'Save and Notify';
$labels['addsubtask'] = 'Add subtask';
$labels['deletetask'] = 'Delete task';
$labels['deletethisonly'] = 'Delete this task only';
@@ -88,6 +89,9 @@ $labels['deleteparenttasktconfirm'] = 'Do you really want to delete this task an
$labels['deletelistconfirm'] = 'Do you really want to delete this list with all its tasks?';
$labels['deletelistconfirmrecursive'] = 'Do you really want to delete this list with all its sub-lists and tasks?';
$labels['aclnorights'] = 'You do not have administrator rights on this task list.';
+$labels['changetaskconfirm'] = 'Update task';
+$labels['changeconfirmnotifications'] = 'Do you want to notify the attendees about the modification?';
+$labels['partstatupdatenotification'] = 'Do you want to notify the organizer about the status change?';
// (hidden) titles and labels for accessibility annotations
$labels['quickaddinput'] = 'New task date and title';
@@ -124,17 +128,27 @@ $labels['saveintasklist'] = 'save in ';
// invitation handling (overrides labels from libcalendaring)
$labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
-$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nDue: \$date";
+$labels['itipmailbodyin-process'] = "\$sender has set the status of the following task to in-process:\n\n*\$title*\n\nDue: \$date";
+$labels['itipmailbodycompleted'] = "\$sender has completed the following task:\n\n*\$title*\n\nDue: \$date";
-$labels['itipdeclineevent'] = 'Do you want to decline your assignment to this task?';
+$labels['attendeeaccepted'] = 'Assignee has accepted';
+$labels['attendeetentative'] = 'Assignee has tentatively accepted';
+$labels['attendeedeclined'] = 'Assignee has declined';
+$labels['attendeedelegated'] = 'Assignee has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Assignee is in-process';
+$labels['attendeecompleted'] = 'Assignee has completed';
+
+$labels['itipdeclinetask'] = 'Decline your assignment to this task to the organizer';
$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
$labels['itipcomment'] = 'Invitation/notification comment';
-$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants';
-$labels['itipsendsuccess'] = 'Invitation sent to participants.';
-$labels['errornotifying'] = 'Failed to send notifications to task participants';
+$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to assignees';
+$labels['itipsendsuccess'] = 'Invitation sent to assignees';
+$labels['errornotifying'] = 'Failed to send notifications to task assignees';
+$labels['removefromcalendar'] = 'Remove from my tasks';
$labels['andnmore'] = '$nr more...';
$labels['delegatedto'] = 'Delegated to: ';
@@ -149,7 +163,7 @@ $labels['nowritetasklistfound'] = 'No tasklist found to save the task';
$labels['importedsuccessfully'] = 'The task was successfully added to \'$list\'';
$labels['updatedsuccessfully'] = 'The task was successfully updated in \'$list\'';
$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
-$labels['itipresponseerror'] = 'Failed to send the response to this task invitation';
+$labels['itipresponseerror'] = 'Failed to send the response to this task assignment';
$labels['itipinvalidrequest'] = 'This invitation is no longer valid';
-$labels['sentresponseto'] = 'Successfully sent invitation response to $mailto';
+$labels['sentresponseto'] = 'Successfully sent assignment response to $mailto';
$labels['successremoval'] = 'The task has been deleted successfully.';
diff --git a/plugins/tasklist/skins/larry/images/attendee-status.png b/plugins/tasklist/skins/larry/images/attendee-status.png
index 59b4493..5343e60 100644
Binary files a/plugins/tasklist/skins/larry/images/attendee-status.png and b/plugins/tasklist/skins/larry/images/attendee-status.png differ
diff --git a/plugins/tasklist/skins/larry/images/badge_cancelled.png b/plugins/tasklist/skins/larry/images/badge_cancelled.png
new file mode 100644
index 0000000..b89029e
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/badge_cancelled.png differ
diff --git a/plugins/tasklist/skins/larry/images/ical-attachment.png b/plugins/tasklist/skins/larry/images/ical-attachment.png
deleted file mode 100644
index 8fa486a..0000000
Binary files a/plugins/tasklist/skins/larry/images/ical-attachment.png and /dev/null differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 1d891ed..3953977 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -815,6 +815,10 @@ ul.toolbarmenu li span.delete {
color: #999;
}
+#taskshow.status-cancelled {
+ background: url(images/badge_cancelled.png) top right no-repeat;
+}
+
#task-parent-title {
position: relative;
top: -0.6em;
@@ -1043,7 +1047,7 @@ label.block {
text-decoration: underline;
}
-.task-attendees span.accepted {
+.task-attendees span.completed {
background-position: right -20px;
}
@@ -1059,6 +1063,14 @@ label.block {
background-position: right -180px;
}
+.task-attendees span.in-process {
+ background-position: right -200px;
+}
+
+.task-attendees span.accepted {
+ background-position: right -220px;
+}
+
.task-attendees span.organizer {
background-position: right 100px;
}
@@ -1082,12 +1094,7 @@ label.block {
width: 20em;
}
-.ui-dialog .task-update-confirm {
- padding: 0 0.5em 0.5em 0.5em;
-}
-
-.task-dialog-message,
-.task-update-confirm .message {
+.task-dialog-message {
margin-top: 0.5em;
padding: 0.8em;
border: 1px solid #ffdf0e;
@@ -1161,35 +1168,54 @@ div.tasklist-invitebox .rsvp-status.hint {
}
#event-partstat .changersvp,
+.edit-attendees-table td.confirmstate span,
div.tasklist-invitebox .rsvp-status.declined,
div.tasklist-invitebox .rsvp-status.tentative,
div.tasklist-invitebox .rsvp-status.accepted,
div.tasklist-invitebox .rsvp-status.delegated,
-div.tasklist-invitebox .rsvp-status.needs-action {
+div.tasklist-invitebox .rsvp-status.in-process,
+div.tasklist-invitebox .rsvp-status.completed,
+div.tasklist-invitebox .rsvp-status.needs-action {
padding: 0 0 1px 22px;
background: url(images/attendee-status.png) 2px -20px no-repeat;
}
#event-partstat .changersvp.declined,
-div.tasklist-invitebox .rsvp-status.declined {
+div.tasklist-invitebox .rsvp-status.declined,
+.edit-attendees-table td.confirmstate span.declined {
background-position: 2px -40px;
}
#event-partstat .changersvp.tentative,
-div.tasklist-invitebox .rsvp-status.tentative {
+div.tasklist-invitebox .rsvp-status.tentative,
+.edit-attendees-table td.confirmstate span.tentative {
background-position: 2px -60px;
}
#event-partstat .changersvp.delegated,
-div.tasklist-invitebox .rsvp-status.delegated {
+div.tasklist-invitebox .rsvp-status.delegated,
+.edit-attendees-table td.confirmstate span.delegated {
background-position: 2px -180px;
}
#event-partstat .changersvp.needs-action,
-div.tasklist-invitebox .rsvp-status.needs-action {
+div.tasklist-invitebox .rsvp-status.needs-action,
+.edit-attendees-table td.confirmstate span.needs-action {
background-position: 2px 0;
}
+#event-partstat .changersvp.in-process,
+div.tasklist-invitebox .rsvp-status.in-process,
+.edit-attendees-table td.confirmstate span.in-process {
+ background-position: 2px -200px;
+}
+
+#event-partstat .changersvp.accepted,
+div.tasklist-invitebox .rsvp-status.accepted,
+.edit-attendees-table td.confirmstate span.accepted {
+ background-position: 2px -220px;
+}
+
/** Special hacks for IE7 **/
/** They need to be in this file to also affect the task-create dialog embedded in mail view **/
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 1b10773..9da7a88 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -353,9 +353,8 @@ function rcube_tasklist_ui(settings)
if (rcmail.busy)
return false;
- rec.status = e.target.checked ? 'COMPLETED' : (rec.complete == 1 ? 'NEEDS-ACTION' : '');
- li.toggleClass('complete');
- save_task(rec, 'edit');
+ save_task_confirm(rec, 'edit', { _status_before:rec.status + '', status:e.target.checked ? 'COMPLETED' : (rec.complete > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION') });
+ item.toggleClass('complete');
return true;
case 'flagged':
@@ -363,7 +362,7 @@ function rcube_tasklist_ui(settings)
return false;
rec.flagged = rec.flagged ? 0 : 1;
- li.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
+ item.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
save_task(rec, 'edit');
break;
@@ -377,8 +376,7 @@ function rcube_tasklist_ui(settings)
input.datepicker($.extend({
onClose: function(dateText, inst) {
if (dateText != (rec.date || '')) {
- rec.date = dateText;
- save_task(rec, 'edit');
+ save_task_confirm(rec, 'edit', { date:dateText });
}
input.datepicker('destroy').remove();
link.html(dateText || rcmail.gettext('nodate','tasklist'));
@@ -971,6 +969,10 @@ function rcube_tasklist_ui(settings)
*/
function save_task(rec, action)
{
+ // show confirmation dialog when status of an assigned task has changed
+ if (rec._status_before !== undefined && is_attendee(rec))
+ return save_task_confirm(rec, action);
+
if (!rcmail.busy) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask });
@@ -982,6 +984,84 @@ function rcube_tasklist_ui(settings)
}
/**
+ * Display confirm dialog when modifying/deleting a task record
+ */
+ var save_task_confirm = function(rec, action, updates)
+ {
+ var data = $.extend({}, rec, updates || {}),
+ notify = false, partstat = false, html = '';
+
+ // task has attendees, ask whether to notify them
+ if (has_attendees(rec)) {
+ if (is_organizer(rec)) {
+ notify = true;
+ html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
+ }
+ // ask whether to change my partstat and notify organizer
+ else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
+ partstat = true;
+ html = rcmail.gettext('partstatupdatenotification', 'tasklist');
+ }
+ }
+
+ // remove to avoid endless recursion
+ delete data._status_before;
+
+ // show dialog
+ if (html) {
+ var $dialog = $('<div>').html(html);
+
+ var buttons = [];
+ buttons.push({
+ text: rcmail.gettext('saveandnotify', 'tasklist'),
+ click: function() {
+ if (notify) data._notify = 1;
+ if (partstat) data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status;
+ save_task(data, action);
+ $(this).dialog('close');
+ }
+ });
+ buttons.push({
+ text: rcmail.gettext('save', 'tasklist'),
+ click: function() {
+ save_task(data, action);
+ $(this).dialog('close');
+ }
+ });
+ buttons.push({
+ text: rcmail.gettext('cancel', 'tasklist'),
+ click: function() {
+ $(this).dialog('close');
+ if (updates)
+ render_task(rec, rec.id); // restore previous state
+ }
+ });
+
+ $dialog.dialog({
+ modal: true,
+ width: 460,
+ closeOnEscapeType: false,
+ dialogClass: 'warning no-close',
+ title: rcmail.gettext('changetaskconfirm', 'tasklist'),
+ buttons: buttons,
+ open: function() {
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function(){
+ $dialog.dialog('destroy').remove();
+ }
+ }).addClass('task-update-confirm').show();
+
+ return true;
+ }
+
+ // do update
+ return save_task(data, action);
+ }
+
+ /**
* Remove saving lock and free the UI for new input
*/
function unlock_saving()
@@ -1488,6 +1568,12 @@ function rcube_tasklist_ui(settings)
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
+ // remove status-* classes
+ $dialog.removeClass(function(i, oldclass) {
+ var oldies = String(oldclass).split(' ');
+ return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' ');
+ });
+
if (!(rec = listdata[id]) || clear_popups({}))
return;
@@ -1528,6 +1614,10 @@ function rcube_tasklist_ui(settings)
});
}
+ if (rec.status) {
+ $dialog.addClass('status-' + String(rec.status).toLowerCase());
+ }
+
if (rec.recurrence && rec.recurrence_text) {
$('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
}
@@ -1817,6 +1907,7 @@ function rcube_tasklist_ui(settings)
var buttons = {};
buttons[rcmail.gettext('save', 'tasklist')] = function() {
var data = me.selected_task;
+ data._status_before = me.selected_task.status + '';
// copy form field contents into task object to save
$.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus, list:tasklist }, function(key,input){
@@ -1867,6 +1958,8 @@ function rcube_tasklist_ui(settings)
data.complete = complete.val() / 100;
if (isNaN(data.complete))
data.complete = null;
+ else if (data.complete == 1.0 && rec.status === '')
+ data.status = 'COMPLETED';
if (!data.list && list.id)
data.list = list.id;
@@ -1879,11 +1972,6 @@ function rcube_tasklist_ui(settings)
delete data.organizer;
}
- // don't submit attendees if only myself is added as organizer
- if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && String(data.attendees[0].email).toLowerCase() == settings.identity.email) {
- data.attendees = [];
- }
-
// per-attendee notification suppression
var need_invitation = false;
if (allow_invitations) {
@@ -2050,7 +2138,34 @@ function rcube_tasklist_ui(settings)
if (!rec || rec.readonly || rcmail.busy)
return false;
- var html, buttons = [];
+ var html, buttons = [], $dialog = $('<div>');
+
+ // Subfunction to submit the delete command after confirm
+ var _delete_task = function(id, mode) {
+ var rec = listdata[id],
+ li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide(),
+ decline = $dialog.find('input.confirm-attendees-decline:checked').length,
+ notify = $dialog.find('input.confirm-attendees-notify:checked').length;
+
+ saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
+ rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list, _decline:decline, _notify:notify }, mode:mode, filter:filtermask });
+
+ // move childs to parent/root
+ if (mode != 1 && rec.children !== undefined) {
+ var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
+ if (!parent_node || !parent_node.length)
+ parent_node = rcmail.gui_objects.resultlist;
+
+ $.each(rec.children, function(i,cid) {
+ var child = listdata[cid];
+ child.parent_id = rec.parent_id;
+ resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
+ });
+ }
+
+ li.remove();
+ delete listdata[id];
+ }
if (rec.children && rec.children.length) {
html = rcmail.gettext('deleteparenttasktconfirm','tasklist');
@@ -2080,6 +2195,19 @@ function rcube_tasklist_ui(settings)
});
}
+ if (is_attendee(rec)) {
+ html += '<div class="task-dialog-message">' +
+ '<label><input class="confirm-attendees-decline" type="checkbox" checked="checked" value="1" name="_decline" /> ' +
+ rcmail.gettext('itipdeclinetask', 'tasklist') +
+ '</label></div>';
+ }
+ else if (has_attendees(rec) && is_organizer(rec)) {
+ html += '<div class="task-dialog-message">' +
+ '<label><input class="confirm-attendees-notify" type="checkbox" checked="checked" value="1" name="_notify" /> ' +
+ rcmail.gettext('sendcancellation', 'tasklist') +
+ '</label></div>';
+ }
+
buttons.push({
text: rcmail.gettext('cancel', 'tasklist'),
click: function() {
@@ -2087,11 +2215,11 @@ function rcube_tasklist_ui(settings)
}
});
- var $dialog = $('<div>').html(html);
+ $dialog.html(html);
$dialog.dialog({
modal: true,
width: 520,
- dialogClass: 'warning',
+ dialogClass: 'warning no-close',
title: rcmail.gettext('deletetask', 'tasklist'),
buttons: buttons,
close: function(){
@@ -2103,34 +2231,6 @@ function rcube_tasklist_ui(settings)
}
/**
- * Subfunction to submit the delete command after confirm
- */
- function _delete_task(id, mode)
- {
- var rec = listdata[id],
- li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
-
- saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
- rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list }, mode:mode, filter:filtermask });
-
- // move childs to parent/root
- if (mode != 1 && rec.children !== undefined) {
- var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
- if (!parent_node || !parent_node.length)
- parent_node = rcmail.gui_objects.resultlist;
-
- $.each(rec.children, function(i,cid) {
- var child = listdata[cid];
- child.parent_id = rec.parent_id;
- resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
- });
- }
-
- li.remove();
- delete listdata[id];
- }
-
- /**
* Check if the given task matches the current filtermask and tag selection
*/
function match_filter(rec, cache, recursive)
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 62ddea0..9ac70bc 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -342,6 +342,24 @@ class tasklist extends rcube_plugin
$this->rc->output->show_message('tasklist.errornotifying', 'error');
}
}
+ else if ($success && $rec['_reportpartstat']) {
+ // get the full record after update
+ $task = $this->driver->get_task($rec);
+
+ // send iTip REPLY with the updated partstat
+ if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) {
+ $sender = $task['attendees'][$idx];
+ $status = strtolower($sender['status']);
+
+ $itip = $this->load_itip();
+ $itip->set_sender_email($sender['email']);
+
+ if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
+ else
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+ }
// unlock client
$this->rc->output->command('plugin.unlock_saving');
@@ -367,6 +385,7 @@ class tasklist extends rcube_plugin
require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
$this->itip = new libcalendaring_itip($this, 'tasklist');
$this->itip->set_rsvp_actions(array('accepted','declined'));
+ $this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
}
return $this->itip;
@@ -517,6 +536,20 @@ class tasklist extends rcube_plugin
$rec['attachments'] = $attachments;
+ // convert invalid data
+ if (isset($rec['attendees']) && !is_array($rec['attendees']))
+ $rec['attendees'] = array();
+
+ // copy the task status to my attendee partstat
+ if (!empty($rec['_reportpartstat'])) {
+ if (($idx = $this->is_attendee($rec)) !== false) {
+ if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED'))
+ $rec['attendees'][$idx]['status'] = $rec['_reportpartstat'];
+ else
+ unset($rec['_reportpartstat']);
+ }
+ }
+
// set organizer from identity selector
if (isset($rec['_identity']) && ($identity = $this->rc->user->get_identity($rec['_identity']))) {
$rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']);
@@ -1044,9 +1077,25 @@ class tasklist extends rcube_plugin
else if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit))
$mask |= self::FILTER_MASK_LATER;
+ // TODO: add mask for "assigned to me"
+
return $mask;
}
+ /**
+ * Determine whether the current user is an attendee of the given task
+ */
+ public function is_attendee($task)
+ {
+ $emails = $this->lib->get_user_emails();
+ foreach ((array)$task['attendees'] as $i => $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ return $i;
+ }
+ }
+
+ return false;
+ }
/******* UI functions ********/
@@ -1709,7 +1758,7 @@ class tasklist extends rcube_plugin
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
$metadata['rsvp'] = intval($metadata['rsvp']);
- $metadata['after_action'] = $this->rc->config->get('tasklist_itip_after_action');
+ $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
$this->rc->output->command('plugin.itip_message_processed', $metadata);
$error_msg = null;
@@ -1725,7 +1774,7 @@ class tasklist extends rcube_plugin
$itip->set_sender_email($reply_sender);
if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
- $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
@@ -1813,6 +1862,7 @@ class tasklist extends rcube_plugin
public function to_libcal($task)
{
$object = $task;
+ $object['_type'] = 'task';
$object['categories'] = (array)$task['tags'];
// convert to datetime objects
commit e46cc9499efb8539be4a8701155c32fabbd81d28
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date: Thu Jul 31 11:36:18 2014 +0200
Add support for task-specific participant status values
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index ece0e48..93bdfee 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -60,6 +60,11 @@ class libcalendaring_itip
$this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
}
+ public function set_rsvp_status($status)
+ {
+ $this->rsvp_status = $status;
+ }
+
/**
* Wrapper for rcube_plugin::gettext()
* Checking for a label in different domains
@@ -246,7 +251,8 @@ class libcalendaring_itip
// attach ics file for this event
$ical = libcalendaring::get_ical();
$ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
- $message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
+ $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
+ $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
return $message;
}
@@ -321,9 +327,10 @@ class libcalendaring_itip
$listed = false;
foreach ($existing['attendees'] as $attendee) {
if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
- if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED','DELEGATED'))) {
- $html = html::div('rsvp-status ' . strtolower($status), $this->gettext(array(
- 'name' => 'attendee'.strtolower($status),
+ $status_lc = strtolower($status);
+ if (in_array($status_lc, $this->rsvp_status)) {
+ $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
+ 'name' => 'attendee' . $status_lc,
'vars' => array(
'delegatedto' => Q($attendee['delegated-to'] ?: '?'),
)
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 7e8c717..f3a506b 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -86,6 +86,8 @@ $labels['itipobjectnotfound'] = 'The object referred by this message was not fou
$labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
$labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
$labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
+$labels['itipsubjectin-process'] = '"$title" is in-process by $name';
+$labels['itipsubjectcompleted'] = '"$title" was completed by $name';
$labels['itipsubjectcancel'] = 'Your participation in "$title" has been cancelled';
$labels['itipnewattendee'] = 'This is a reply from a new participant';
@@ -99,18 +101,24 @@ $labels['youhaveaccepted'] = 'You have accepted this invitation';
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
$labels['youhavedeclined'] = 'You have declined this invitation';
$labels['youhavedelegated'] = 'You have delegated this invitation';
+$labels['youhavein-process'] = 'You are working on this assignment';
+$labels['youhavecompleted'] = 'You have completed this assignment';
$labels['youhaveneeds-action'] = 'Your response to this invitation is still pending';
$labels['youhavepreviouslyaccepted'] = 'You have previously accepted this invitation';
$labels['youhavepreviouslytentative'] = 'You have previously accepted this invitation tentatively';
$labels['youhavepreviouslydeclined'] = 'You have previously declined this invitation';
$labels['youhavepreviouslydelegated'] = 'You have previously delegated this invitation';
+$labels['youhavepreviouslyin-process'] = 'You have previously reported to work on this assignment';
+$labels['youhavepreviouslycompleted'] = 'You have previously completed this assignment';
$labels['youhavepreviouslyneeds-action'] = 'Your response to this invitation is still pending';
$labels['attendeeaccepted'] = 'Participant has accepted';
$labels['attendeetentative'] = 'Participant has tentatively accepted';
$labels['attendeedeclined'] = 'Participant has declined';
$labels['attendeedelegated'] = 'Participant has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Participant is in-process';
+$labels['attendeecompleted'] = 'Participant has completed';
$labels['notanattendee'] = 'You\'re not listed as an attendee of this object';
$labels['outdatedinvitation'] = 'This invitation has been replaced by a newer version';
More information about the commits
mailing list