- Fix task attendees and organizer setting and display

- Make basic iTip exchange for task assignments work
- Improve wording for task assignments
This commit is contained in:
Thomas Bruederli 2014-07-30 17:40:53 +02:00
parent 445edd09b7
commit b3c5acd66a
11 changed files with 242 additions and 161 deletions

View file

@ -213,6 +213,14 @@ class libcalendaring_itip
$event['attendees'] = $reply_attendees; $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 // compose multipart message using PEAR:Mail_Mime
$message = new Mail_mime("\r\n"); $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) function itip_object_details_table($event, $title)
{ {
$table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails')); $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
$table->add('ititle', $title); $table->add('ititle', $title);
$table->add('title', Q($event['title'])); $table->add('title', Q($event['title']));
$table->add('label', $this->plugin->gettext('date'), $this->domain); if ($event['start'] && $event['end']) {
$table->add('date', Q($this->lib->event_date_text($event))); $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']) { if ($event['location']) {
$table->add('label', $this->plugin->gettext('location'), $this->domain); $table->add('label', $this->plugin->gettext('location'), $this->domain);
$table->add('location', Q($event['location'])); $table->add('location', Q($event['location']));

View file

@ -247,11 +247,25 @@ class libcalendaring extends rcube_plugin
*/ */
public function event_date_text($event, $tzinfo = false) 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 // 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; return $fromto;
}
$duration = $event['start']->diff($event['end'])->format('s'); $duration = $event['start']->diff($event['end'])->format('s');

View file

@ -567,7 +567,7 @@ class libvcalendar implements Iterator
} }
// make organizer part of the attendees list for compatibility reasons // 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']); array_unshift($event['attendees'], $event['organizer']);
} }

View file

@ -774,6 +774,8 @@ class tasklist_kolab_driver extends tasklist_driver
'parent_id' => $record['parent_id'], 'parent_id' => $record['parent_id'],
'recurrence' => $record['recurrence'], 'recurrence' => $record['recurrence'],
'attendees' => $record['attendees'], 'attendees' => $record['attendees'],
'organizer' => $record['organizer'],
'sequence' => $record['sequence'],
); );
// convert from DateTime to internal date format // 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 * 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()) * (opposite of self::_to_rcube_event())
*/ */
private function _from_rcube_task($task, $old = array()) private function _from_rcube_task($task, $old = array())
{ {
@ -826,14 +828,14 @@ class tasklist_kolab_driver extends tasklist_driver
$object['categories'] = (array)$task['tags']; $object['categories'] = (array)$task['tags'];
if (!empty($task['date'])) { 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'])) if (empty($task['time']))
$object['due']->_dateonly = true; $object['due']->_dateonly = true;
unset($object['date']); unset($object['date']);
} }
if (!empty($task['startdate'])) { 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'])) if (empty($task['starttime']))
$object['start']->_dateonly = true; $object['start']->_dateonly = true;
unset($object['startdate']); unset($object['startdate']);
@ -900,12 +902,6 @@ class tasklist_kolab_driver extends tasklist_driver
unset($object['attachments']); 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']; $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']);

View file

@ -33,6 +33,7 @@ $labels['status-needs-action'] = 'Needs action';
$labels['status-in-process'] = 'In process'; $labels['status-in-process'] = 'In process';
$labels['status-completed'] = 'Completed'; $labels['status-completed'] = 'Completed';
$labels['status-cancelled'] = 'Cancelled'; $labels['status-cancelled'] = 'Cancelled';
$labels['assignedto'] = 'Assigned to';
$labels['all'] = 'All'; $labels['all'] = 'All';
$labels['flagged'] = 'Flagged'; $labels['flagged'] = 'Flagged';
@ -98,35 +99,35 @@ $labels['arialabeltaskselector'] = 'List mode';
$labels['arialabeltasklisting'] = 'Tasks listing'; $labels['arialabeltasklisting'] = 'Tasks listing';
// attendees // attendees
$labels['attendee'] = 'Participant'; $labels['attendee'] = 'Assignee';
$labels['role'] = 'Role'; $labels['role'] = 'Role';
$labels['availability'] = 'Avail.'; $labels['availability'] = 'Avail.';
$labels['confirmstate'] = 'Status'; $labels['confirmstate'] = 'Status';
$labels['addattendee'] = 'Add participant'; $labels['addattendee'] = 'Add assignee';
$labels['roleorganizer'] = 'Organizer'; $labels['roleorganizer'] = 'Organizer';
$labels['rolerequired'] = 'Required'; $labels['rolerequired'] = 'Required';
$labels['roleoptional'] = 'Optional'; $labels['roleoptional'] = 'Optional';
$labels['rolechair'] = 'Chair'; $labels['rolechair'] = 'Chair';
$labels['rolenonparticipant'] = 'Absent'; $labels['rolenonparticipant'] = 'Observer';
$labels['sendinvitations'] = 'Send invitations'; $labels['sendinvitations'] = 'Send invitations';
$labels['sendnotifications'] = 'Notify participants about modifications'; $labels['sendnotifications'] = 'Notify assignees about modifications';
$labels['sendcancellation'] = 'Notify participants about task cancellation'; $labels['sendcancellation'] = 'Notify assignees about task cancellation';
$labels['invitationsubject'] = 'You\'ve been invited to "$title"'; $labels['invitationsubject'] = 'You\'ve been assigned 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['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['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['itipupdatesubject'] = '"$title" has been updated';
$labels['eventupdatesubject'] = '"$title" has been updated'; $labels['itipupdatesubjectempty'] = 'A task that concerns you has been updated';
$labels['eventupdatesubjectempty'] = '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['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['itipcancelsubject'] = '"$title" has been canceled';
$labels['eventcancelsubject'] = '"$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['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['saveintasklist'] = 'save in ';
// invitation handling (overrides labels from libcalendaring) // invitation handling (overrides labels from libcalendaring)
$labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.'; $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['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\nWhen: \$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\nWhen: \$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\nWhen: \$date"; $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['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['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';

View file

@ -855,6 +855,14 @@ a.morelink:hover {
margin-top: 0.5em; 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 th.role,
.edit-attendees-table td.role { .edit-attendees-table td.role {
width: 9em; width: 9em;
@ -864,18 +872,19 @@ a.morelink:hover {
.edit-attendees-table td.availability, .edit-attendees-table td.availability,
.edit-attendees-table th.confirmstate, .edit-attendees-table th.confirmstate,
.edit-attendees-table td.confirmstate { .edit-attendees-table td.confirmstate {
width: 4em; width: 6em;
} }
.edit-attendees-table th.options, .edit-attendees-table th.options,
.edit-attendees-table td.options { .edit-attendees-table td.options {
width: 16px; width: 24px;
padding: 2px 4px; padding: 2px 4px;
text-align: right;
} }
.edit-attendees-table th.sendmail, .edit-attendees-table th.sendmail,
.edit-attendees-table td.sendmail { .edit-attendees-table td.sendmail {
width: 44px; width: 48px;
padding: 2px; padding: 2px;
} }
@ -955,7 +964,7 @@ a.morelink:hover {
div.form-section { div.form-section {
position: relative; position: relative;
margin-top: 0.2em; margin-top: 0.2em;
margin-bottom: 0.8em; margin-bottom: 0.5em;
} }
.form-section label { .form-section label {
@ -970,6 +979,10 @@ label.block {
margin-bottom: 0.3em; margin-bottom: 0.3em;
} }
#task-description {
margin-bottom: 1em;
}
#taskedit-completeness-slider { #taskedit-completeness-slider {
display: inline-block; display: inline-block;
margin-left: 2em; margin-left: 2em;
@ -1047,7 +1060,7 @@ label.block {
} }
.task-attendees span.organizer { .task-attendees span.organizer {
background-position: right -80px; background-position: right 100px;
} }
#all-task-attendees span.attendee { #all-task-attendees span.attendee {

View file

@ -156,12 +156,16 @@
<label><roundcube:label name="tasklist.alarms" /></label> <label><roundcube:label name="tasklist.alarms" /></label>
<span class="task-text"></span> <span class="task-text"></span>
</div> </div>
<div class="form-section task-attendees" id="task-attendees"> <div id="task-attendees" class="form-section task-attendees">
<h5 class="label"><roundcube:label name="tasklist.tabassignments" /></h5> <label><roundcube:label name="tasklist.assignedto" /></label>
<div class="task-text"></div> <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>
<!-- <!--
<div class="form-section" id="task-partstat"> <div id="task-partstat" class="form-section">
<label><roundcube:label name="tasklist.mystatus" /></label> <label><roundcube:label name="tasklist.mystatus" /></label>
<span class="changersvp" role="button" tabindex="0" title="<roundcube:label name='tasklist.changepartstat' />"> <span class="changersvp" role="button" tabindex="0" title="<roundcube:label name='tasklist.changepartstat' />">
<span class="task-text"></span> <span class="task-text"></span>

View file

@ -81,6 +81,10 @@
</div> </div>
<!-- attendees list (assignments) --> <!-- attendees list (assignments) -->
<div id="taskedit-panel-attendees"> <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> <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_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:object name="plugin.attendees_form" id="edit-attendees-form" />

View file

@ -84,7 +84,6 @@ function rcube_tasklist_ui(settings)
var focused_subclass; var focused_subclass;
var task_attendees = []; var task_attendees = [];
var attendees_list; var attendees_list;
// var resources_list;
var me = this; var me = this;
// general datepicker settings // general datepicker settings
@ -1349,7 +1348,7 @@ function rcube_tasklist_ui(settings)
}; };
// check if the current user is an attendee of this task // 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; 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 // check if the current user is the organizer
var is_organizer = function(task, email) 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 // add the given list of participants
@ -1421,34 +1423,10 @@ function rcube_tasklist_ui(settings)
if (exists) if (exists)
return false; 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); var dispname = Q(data.name || data.email);
if (data.email) if (data.email)
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>'; 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 // delete icon
var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete'); 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 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']) else if (data['delegated-from'])
tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from']; tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
var html = '<td class="role">' + select + '</td>' + var html = '<td class="name">' + dispname + '</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>' + '<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>' : '') + (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (readonly || !invbox ? '' : invbox) + '</td>' : '') +
'<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>'; '<td class="options">' + (readonly ? '' : dellink) + '</td>';
var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
var tr = $('<tr>') var tr = $('<tr>')
.addClass(String(data.role).toLowerCase()) .addClass(String(data.role).toLowerCase())
.html(html) .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.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('a.mailtolink').click(task_attendee_click);
@ -1483,15 +1458,6 @@ function rcube_tasklist_ui(settings)
$('p.attendees-commentbox')[enabled ? 'show' : 'hide'](); $('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); task_attendees.push(data);
return true; return true;
}; };
@ -1499,15 +1465,8 @@ function rcube_tasklist_ui(settings)
// event handler for clicks on an attendee link // event handler for clicks on an attendee link
var task_attendee_click = function(e) var task_attendee_click = function(e)
{ {
var cutype = $(this).attr('data-cutype'), var mailto = this.href.substr(7);
mailto = this.href.substr(7); rcmail.command('compose', mailto);
if (rcmail.env.tasklist_resources && cutype == 'RESOURCE') {
task_resources_dialog(mailto);
}
else {
rcmail.redirect(rcmail.url('mail/compose', {_to: mailto}));
}
return false; return false;
}; };
@ -1547,7 +1506,7 @@ function rcube_tasklist_ui(settings)
$('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%'); $('#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-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-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 itags = get_inherited_tags(rec);
var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty(); 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 // list task attendees
if (list.attendees && rec.attendees) { if (list.attendees && rec.attendees) {
console.log(rec.attendees)
/* /*
// sort resources to the end // sort resources to the end
rec.attendees.sort(function(a,b) { rec.attendees.sort(function(a,b) {
@ -1595,30 +1555,19 @@ function rcube_tasklist_ui(settings)
return (j - k); 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++) { for (j=0; j < rec.attendees.length; j++) {
data = rec.attendees[j]; data = rec.attendees[j];
dispname = Q(data.name || data.email);
tooltip = '';
if (data.email) { if (data.email && settings.identity.emails.indexOf(';'+data.email) >= 0) {
tooltip = data.email; mystatus = data.status.toLowerCase();
dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>'; if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
if (data.role == 'ORGANIZER') rsvp = mystatus;
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']) line = task_attendee_html(data);
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) if (morelink)
overflow += line; overflow += line;
@ -1631,7 +1580,7 @@ function rcube_tasklist_ui(settings)
} }
} }
if (html && (rec.attendees.length > 1 || !organizer)) { if (html) {
$('#task-attendees').show() $('#task-attendees').show()
.children('.task-text') .children('.task-text')
.html(html) .html(html)
@ -1667,6 +1616,10 @@ function rcube_tasklist_ui(settings)
$('#task-rsvp a.reply-comment-toggle').show(); $('#task-rsvp a.reply-comment-toggle').show();
$('#task-rsvp .itip-reply-comment textarea').hide().val(''); $('#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 // define dialog buttons
@ -1697,7 +1650,7 @@ function rcube_tasklist_ui(settings)
closeOnEscape: true, closeOnEscape: true,
title: rcmail.gettext('taskdetails', 'tasklist'), title: rcmail.gettext('taskdetails', 'tasklist'),
open: function() { 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() { close: function() {
$dialog.dialog('destroy').appendTo(document.body); $dialog.dialog('destroy').appendTo(document.body);
@ -1711,6 +1664,24 @@ function rcube_tasklist_ui(settings)
me.dialog_resize($dialog.get(0), $dialog.height(), 580); 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 * Opens the dialog to edit a task
*/ */
@ -1786,18 +1757,17 @@ function rcube_tasklist_ui(settings)
task_attendees = []; task_attendees = [];
attendees_list = $('#edit-attendees-table > tbody').html(''); attendees_list = $('#edit-attendees-table > tbody').html('');
//resources_list = $('#edit-resources-table > tbody').html('');
$('#edit-attendees-notify')[(notify.checked && allow_invitations ? 'show' : 'hide')](); $('#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')](); $('#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; var j, data, reply_selected = 0;
if (rec.attendees) { if (rec.attendees) {
for (j=0; j < rec.attendees.length; j++) { for (j=0; j < rec.attendees.length; j++) {
data = rec.attendees[j]; data = rec.attendees[j];
add_attendee(data, !allow_invitations); add_attendee(data, !allow_invitations);
if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply) { if (allow_invitations && !data.noreply) {
reply_selected++; reply_selected++;
} }
} }
@ -1812,16 +1782,16 @@ function rcube_tasklist_ui(settings)
// select the correct organizer identity // select the correct organizer identity
var identity_id = 0; var identity_id = 0;
$.each(settings.identities, function(i,v) { $.each(settings.identities, function(i,v) {
if (organizer && v == organizer.email) { if (rec.organizer && v == rec.organizer.email) {
identity_id = i; identity_id = i;
return false; return false;
} }
}); });
$('#edit-identities-list').val(identity_id);
$('#edit-attendees-form')[(allow_invitations?'show':'hide')](); $('#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 // attachments
rcmail.enable_command('remove-attachment', list.editable); rcmail.enable_command('remove-attachment', list.editable);
@ -1904,15 +1874,9 @@ function rcube_tasklist_ui(settings)
if (!data.tags.length) if (!data.tags.length)
data.tags = ''; 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) { if (organizer) {
data._identity = $('#edit-identities-list option:selected').val(); data._identity = $('#edit-identities-list option:selected').val();
delete data.organizer;
} }
// don't submit attendees if only myself is added as 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 // set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 580); me.dialog_resize($dialog.get(0), $dialog.height(), 580);
if (list.attendees)
window.setTimeout(load_attendees_tab, 1);
} }
/** /**

View file

@ -334,7 +334,7 @@ class tasklist extends rcube_plugin
$task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec); $task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec);
// only notify if data really changed (TODO: do diff check on client already) // 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']); $sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']);
if ($sent > 0) if ($sent > 0)
$this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation'); $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
@ -366,10 +366,7 @@ class tasklist extends rcube_plugin
if (!$this->itip) { if (!$this->itip) {
require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php'); require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
$this->itip = new libcalendaring_itip($this, 'tasklist'); $this->itip = new libcalendaring_itip($this, 'tasklist');
$this->itip->set_rsvp_actions(array('accepted','declined'));
// if ($this->rc->config->get('kolab_invitation_tasklists')) {
// $this->itip->set_rsvp_actions(array('accepted','tentative','declined','needs-action'));
// }
} }
return $this->itip; return $this->itip;
@ -520,6 +517,11 @@ class tasklist extends rcube_plugin
$rec['attachments'] = $attachments; $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) if (is_numeric($rec['id']) && $rec['id'] < 0)
unset($rec['id']); unset($rec['id']);
@ -646,7 +648,8 @@ class tasklist extends rcube_plugin
// compose multipart message using PEAR:Mail_Mime // compose multipart message using PEAR:Mail_Mime
$method = $action == 'delete' ? 'CANCEL' : 'REQUEST'; $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 // list existing attendees from the $old task
$old_attendees = array(); $old_attendees = array();
@ -671,11 +674,11 @@ class tasklist extends rcube_plugin
// which template to use for mail text // which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees); $is_new = !in_array($attendee['email'], $old_attendees);
$bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody'); $bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody');
$subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'eventupdatesubject' : 'eventupdatesubjectempty')); $subject = $is_cancelled ? 'itipcancelsubject' : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty'));
// finally send the message // 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++; $sent++;
else else
$sent = -100; $sent = -100;
@ -683,16 +686,16 @@ class tasklist extends rcube_plugin
// send CANCEL message to removed attendees // send CANCEL message to removed attendees
foreach ((array)$old['attendees'] as $attendee) { 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; continue;
} }
$vevent = $old; $vtodo = $this->to_libcal($old);
$vevent['cancelled'] = $is_cancelled; $vtodo['cancelled'] = $is_cancelled;
$vevent['attendees'] = array($attendee); $vtodo['attendees'] = array($attendee);
$vevent['comment'] = $comment; $vtodo['comment'] = $comment;
if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody')) if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody'))
$sent++; $sent++;
else else
$sent = -100; $sent = -100;
@ -1393,6 +1396,8 @@ class tasklist extends rcube_plugin
// successfully parsed events? // successfully parsed events?
if (!empty($tasks) && ($task = $tasks[$index])) { if (!empty($tasks) && ($task = $tasks[$index])) {
$task = $this->from_ical($task);
// store the message's sender address for comparisons // 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] : ''; $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']); $askt['_sender_utf'] = rcube_idn_to_utf8($task['_sender']);
@ -1496,6 +1501,7 @@ class tasklist extends rcube_plugin
foreach ($tasks as $task) { foreach ($tasks as $task) {
// save to tasklist // save to tasklist
if ($list && $list['editable'] && $task['_type'] == 'task') { if ($list && $list['editable'] && $task['_type'] == 'task') {
$task = $this->from_ical($task);
$task['list'] = $list['id']; $task['list'] = $list['id'];
if (!$this->driver->get_task($task['uid'])) { if (!$this->driver->get_task($task['uid'])) {
@ -1555,14 +1561,11 @@ class tasklist extends rcube_plugin
// update my attendee status according to submitted method // update my attendee status according to submitted method
if (!empty($status)) { if (!empty($status)) {
$organizer = null; $organizer = $task['organizer'];
$emails = $this->lib->get_user_emails(); $emails = $this->lib->get_user_emails();
foreach ($task['attendees'] as $i => $attendee) { foreach ($task['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') { if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$organizer = $attendee;
}
else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
$metadata['attendee'] = $attendee['email']; $metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT'; $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
$reply_sender = $attendee['email']; $reply_sender = $attendee['email'];
@ -1714,7 +1717,7 @@ class tasklist extends rcube_plugin
$itip = $this->load_itip(); $itip = $this->load_itip();
$itip->set_sender_email($reply_sender); $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'); $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error'); $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 * 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); $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 * Handler for calendar/itip-remove requests
*/ */
function task_itip_remove() public function task_itip_remove()
{ {
$success = false; $success = false;
$uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST); $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
@ -1797,6 +1800,78 @@ class tasklist extends rcube_plugin
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16)); return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
} }
/**
* 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 * Handler for user_delete plugin hook
*/ */

View file

@ -56,7 +56,6 @@ class tasklist_ui
// copy config to client // copy config to client
$this->rc->output->set_env('tasklist_settings', $this->load_settings()); $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 // initialize attendees autocompletion
$this->rc->autocomplete_init(); $this->rc->autocomplete_init();
@ -71,7 +70,7 @@ class tasklist_ui
{ {
$settings = array(); $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 // get user identity to create default attendee
foreach ($this->rc->user->list_identities() as $rec) { 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.filedroparea', array($this, 'file_drop_area'));
$this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list')); $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.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->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
$this->plugin->include_script('jquery.tagedit.js'); $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')); $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 = 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('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee'));
// $table->add_header('availability', $this->plugin->gettext('availability'));
$table->add_header('confirmstate', $this->plugin->gettext('confirmstate')); $table->add_header('confirmstate', $this->plugin->gettext('confirmstate'));
if ($invitations) { if ($invitations) {
$table->add_header(array('class' => 'sendmail', 'title' => $this->plugin->gettext('sendinvitations')), $table->add_header(array('class' => 'sendmail', 'title' => $this->plugin->gettext('sendinvitations')),