diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index a5fdd3d4..f2ee6010 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -449,9 +449,9 @@ class calendar extends rcube_plugin
$event['uid'] = $this->generate_uid();
// set current user as organizer
- if (!$event['attendees']) {
+ if (FALSE && !$event['attendees']) {
$identity = $this->rc->user->get_identity();
- $event['attendees'][] = array('role' => 'OWNER', 'name' => $identity['name'], 'email' => $identity['email']);
+ $event['attendees'][] = array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']);
}
$this->prepare_event($event);
@@ -1163,6 +1163,9 @@ class calendar extends rcube_plugin
}
}
+ // let this information be cached for 15min
+ send_future_expire_header(90);
+
echo $status;
exit;
}
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 867516dc..7ee3d250 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -440,7 +440,7 @@ function rcube_calendar_ui(settings)
attendees_list = $('#edit-attendees-table > tbody').html('');
if (calendar.attendees && event.attendees) {
for (var j=0; j < event.attendees.length; j++)
- add_attendee(event.attendees[j]);
+ add_attendee(event.attendees[j], true);
}
// attachments
@@ -509,6 +509,12 @@ function rcube_calendar_ui(settings)
if (i.match(/^rcmfile([0-9a-z]+)/))
attachments.push(RegExp.$1);
data.attachments = attachments;
+
+ // read attendee roles
+ $('select.edit-attendee-role').each(function(i, elem){
+ if (data.attendees[i])
+ data.attendees[i].role = $(elem).val();
+ });
// gather recurrence settings
var freq;
@@ -597,6 +603,19 @@ function rcube_calendar_ui(settings)
title.select();
};
+ // update event properties and attendees availability if event times have changed
+ var event_times_changed = function()
+ {
+ alert('event_times_changed')
+ if (me.selected_event) {
+ var allday = $('#edit-allday').get(0);
+ me.selected_event.start = parse_datetime(allday.checked ? '00:00' : $('#edit-starttime').val(), $('#edit-startdate').val());
+ me.selected_event.end = parse_datetime(allday.checked ? '23:59' : $('#edit-endtime').val(), $('#edit-enddate').val());
+ if (me.selected_event.attendees)
+ update_freebusy_status(me.selected_event);
+ }
+ };
+
// add the given list of participants
var add_attendees = function(names)
{
@@ -623,7 +642,7 @@ function rcube_calendar_ui(settings)
}
if (email) {
- add_attendee({ email:email, name:name, role:'REQUIRED', status:'unknown' });
+ add_attendee({ email:email, name:name, role:'REQ-PARTICIPANT', status:'NEEDS-ACTION' });
success = true;
}
else {
@@ -635,33 +654,93 @@ function rcube_calendar_ui(settings)
};
// add the given attendee to the list
- var add_attendee = function(data)
+ var add_attendee = function(data, edit)
{
- var dispname = (data.email && data.name) ? data.name + ' <' + data.email + '>' : (data.email || data.name);
+ // check for dupes...
+ var exists = false;
+ $.each(event_attendees, function(i, v){ exists |= (v.email == data.email); });
+ if (exists)
+ return false;
+ var dispname = Q(data.name || data.email);
+ if (data.email)
+ dispname = '' + dispname + '';
+
+ // role selection
+ var opts = {
+ 'ORGANIZER': rcmail.gettext('calendar.roleorganizer'),
+ 'REQ-PARTICIPANT': rcmail.gettext('calendar.rolerequired'),
+ 'OPT-PARTICIPANT': rcmail.gettext('calendar.roleoptional'),
+ 'CHAIR': rcmail.gettext('calendar.roleresource')
+ };
+ var select = '';
+
+ // availability
+ var avail = data.email ? 'loading' : 'unknown';
+ if (edit && data.role == 'ORGANIZER' && data.status == 'ACCEPTED')
+ avail = 'free';
+
// delete icon
var icon = rcmail.env.deleteicon ? '
' : rcmail.gettext('delete');
var dellink = '' + icon + '';
- var html = '
| ' +
- '' + Q(dispname) + ' | ' +
- '' + '' + ' | ' +
- '' + Q(data.status) + ' | ' +
- '' + (data.role != 'OWNER' ? dellink : '') + ' | ';
+ var html = '' + select + ' | ' +
+ '' + dispname + ' | ' +
+ ' | ' +
+ '' + Q(data.status) + ' | ' +
+ '' + dellink + ' | ';
- $('')
+ var tr = $('
')
.addClass(String(data.role).toLowerCase())
.html(html)
- .appendTo(attendees_list)
- .find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
+ .appendTo(attendees_list);
+
+ tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
+
+ // check free-busy status
+ if (avail == 'loading') {
+ check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_event);
+ }
event_attendees.push(data);
};
+ // iterate over all attendees and update their free-busy status display
+ var update_freebusy_status = function(event)
+ {
+ var icons = attendees_list.find('img.availabilityicon');
+ for (var i=0; i < event.attendees.length; i++) {
+ if (icons.get(i) && event.attendees[i].email && event.attendees[i].status != 'ACCEPTED')
+ check_freebusy_status(icons.get(i), event.attendees[i].email, event);
+ }
+ };
+
+ // load free-busy status from server and update icon accordingly
+ var check_freebusy_status = function(icon, email, event)
+ {
+ icon = $(icon).removeClass().addClass('availabilityicon loading');
+
+ $.ajax({
+ type: 'GET',
+ dataType: 'html',
+ url: rcmail.url('freebusy-status'),
+ data: { email:email, start:date2unixtime(event.start), end:date2unixtime(event.end), _remote: 1 },
+ success: function(status){
+ icon.removeClass('loading').addClass(String(status).toLowerCase());
+ },
+ error: function(){
+ icon.removeClass('loading').addClass('unknown');
+ }
+ });
+ };
+
// remove an attendee from the list
var remove_attendee = function(elem, id)
{
- $(elem).closest('tr').hide();
+ $(elem).closest('tr').remove();
event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
};
@@ -1234,7 +1313,8 @@ function rcube_calendar_ui(settings)
}
});
$('#edit-enddate, input.edit-alarm-date').datepicker(datepicker_settings);
- $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); });
+ $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); event_times_changed(); });
+ $('#edit-enddate, #edit-starttime, #edit-endtime').change(function(){ event_times_changed(); });
$('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); });
// configure drop-down menu on time input fields based on jquery UI autocomplete
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index 01c68d52..be9bde5e 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -282,6 +282,7 @@ abstract class calendar_driver
*/
public function get_freebusy_list($email, $start, $end)
{
+ sleep(2);
return false;
}
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index d7ebd10a..edc12b12 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -35,6 +35,8 @@ class kolab_calendar
private $search_fields = array('title', 'description', 'location');
private $sensitivity_map = array('public', 'private', 'confidential');
private $priority_map = array('low', 'normal', 'high');
+ private $role_map = array('REQ-PARTICIPANT' => 'required', 'OPT-PARTICIPANT' => 'optional', 'CHAIR' => 'resource');
+ private $status_map = array('NEEDS-ACTION' => 'none', 'TENTATIVE' => 'tentative', 'CONFIRMED' => 'accepted', 'DECLINED' => 'declined');
private $month_map = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
private $weekday_map = array('MO'=>'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday');
@@ -430,6 +432,8 @@ class kolab_calendar
$sensitivity_map = array_flip($this->sensitivity_map);
$priority_map = array_flip($this->priority_map);
+ $status_map = array_flip($this->status_map);
+ $role_map = array_flip($this->role_map);
if (!empty($rec['_attachments'])) {
foreach ($rec['_attachments'] as $name => $attachment) {
@@ -444,22 +448,22 @@ class kolab_calendar
if ($rec['organizer']) {
$attendees[] = array(
- 'role' => 'OWNER',
+ 'role' => 'ORGANIZER',
'name' => $rec['organizer']['display-name'],
'email' => $rec['organizer']['smtp-address'],
- 'status' => 'accepted',
+ 'status' => 'ACCEPTED',
);
}
foreach ((array)$rec['attendee'] as $attendee) {
$attendees[] = array(
- 'role' => strtoupper($attendee['role']),
+ 'role' => $role_map[$attendee['role']],
'name' => $attendee['display-name'],
'email' => $attendee['smtp-address'],
- 'status' => $attendee['status'],
+ 'status' => $status_map[$attendee['status']],
);
}
-
+
return array(
'id' => $rec['uid'],
'uid' => $rec['uid'],
@@ -624,7 +628,7 @@ class kolab_calendar
// process event attendees
foreach ((array)$event['attendees'] as $attendee) {
$role = $attendee['role'];
- if ($role == 'OWNER') {
+ if ($role == 'ORGANIZER') {
$object['organizer'] = array(
'display-name' => $attendee['name'],
'smtp-address' => $attendee['email'],
@@ -634,8 +638,8 @@ class kolab_calendar
$object['attendee'][] = array(
'display-name' => $attendee['name'],
'smtp-address' => $attendee['email'],
- 'status' => $attendee['status'],
- 'role' => strtolower($role),
+ 'status' => $this->status_map[$attendee['status']],
+ 'role' => $this->role_map[$role],
);
}
}
diff --git a/plugins/calendar/lib/js/fullcalendar.js b/plugins/calendar/lib/js/fullcalendar.js
index 36813fa3..e55c47f1 100644
--- a/plugins/calendar/lib/js/fullcalendar.js
+++ b/plugins/calendar/lib/js/fullcalendar.js
@@ -1539,8 +1539,8 @@ function formatDates(date1, date2, format, options) {
for (i2=i+1; i2 1) {
+ segHash = opt('listTexts', 'future');
}
} else if (segmode == 'month') {
segHash = formatDate(segDate, 'MMMM yyyy');
@@ -5447,7 +5449,7 @@ function ListEventRenderer() {
if (event.start < seg.start) {
datestr = opt('listTexts', 'until') + ' ' + formatDate(event.end, (event.allDay || event.end.getDate() != seg.start.getDate()) ? dateFormat : timeFormat);
} else if (duration > DAY_MS) {
- datestr = formatDates(event.start, event.end, dateFormat + '[ - ' + dateFormat + ']');
+ datestr = formatDates(event.start, event.end, dateFormat + '{ - ' + dateFormat + '}');
} else if (seg.daydiff == 0) {
datestr = opt('listTexts', 'today');
} else if (seg.daydiff == 1) {
@@ -5458,7 +5460,7 @@ function ListEventRenderer() {
datestr = formatDate(event.start, dateFormat);
}
} else if (segmode != 'day') {
- datestr = formatDates(event.start, event.end, dateFormat + (duration > DAY_MS ? '[ - ' + dateFormat + ']' : ''));
+ datestr = formatDates(event.start, event.end, dateFormat + (duration > DAY_MS ? '{ - ' + dateFormat + '}' : ''));
}
if (!datestr && event.allDay) {
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 43183ee3..a74d76db 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -90,8 +90,12 @@ $labels['alarmtitle'] = 'Upcoming events';
$labels['attendee'] = 'Participant';
$labels['role'] = 'Role';
$labels['availability'] = 'Avail.';
-$labels['confirmstate'] = 'Confirmed';
+$labels['confirmstate'] = 'Status';
$labels['addattendee'] = 'Add participant';
+$labels['roleorganizer'] = 'Organizer';
+$labels['rolerequired'] = 'Required';
+$labels['roleoptional'] = 'Optional';
+$labels['roleresource'] = 'Resource';
// event dialog tabs
$labels['tabsummary'] = 'Summary';
diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css
index d9aebb71..b155b790 100644
--- a/plugins/calendar/skins/default/calendar.css
+++ b/plugins/calendar/skins/default/calendar.css
@@ -106,7 +106,7 @@ pre {
#calendarslist li span {
cursor: default;
- background: url(images/calendars.png) 0 -3px no-repeat;
+ background: url('images/calendars.png') 0 -3px no-repeat;
padding-left: 18px;
}
@@ -480,11 +480,10 @@ td.topalign {
border-bottom: 1px solid #ddd;
}
-#edit-attendees-table tr.owner td {
- color: #999;
+#edit-attendees-table td.role {
+ width: 8em;
}
-#edit-attendees-table td.role,
#edit-attendees-table td.availability {
width: 4em;
}
@@ -507,13 +506,70 @@ td.topalign {
}
#edit-attendees-table thead td {
- background: url(images/listheader.gif) top left repeat-x #CCC;
+ background: url('images/listheader.gif') top left repeat-x #CCC;
}
#edit-attendees-form {
margin-top: 1em;
}
+#edit-attendees-table select.edit-attendee-role {
+ border: 0;
+ padding: 2px;
+ background: white;
+}
+
+#edit-attendees-table img.availabilityicon {
+ width: 16px;
+ height: 16px;
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+}
+
+#edit-attendees-table img.availabilityicon.loading {
+ background: url('images/loading-small.gif') top left no-repeat;
+}
+
+#edit-attendees-table img.availabilityicon.unknown {
+ background: #ccc;
+}
+
+#edit-attendees-table img.availabilityicon.free {
+ background: #0c0;
+}
+
+#edit-attendees-table img.availabilityicon.busy {
+ background: #c00;
+}
+
+#edit-attendees-table tbody td.confirmstate {
+ overflow: hidden;
+ white-space: nowrap;
+ font-size: 75%;
+/* text-indent: -2000%; */
+}
+
+#edit-attendees-table td.confirmstate span {
+ display: block;
+ width: 20px;
+}
+
+#edit-attendees-table td.confirmstate span.needs-action {
+
+}
+
+#edit-attendees-table td.confirmstate span.tentative {
+
+}
+
+#edit-attendees-table td.confirmstate span.declined {
+
+}
+
+#edit-attendees-table td.confirmstate span.accepted {
+
+}
+
span.edit-alarm-set {
white-space: nowrap;
}
diff --git a/plugins/calendar/skins/default/images/loading-small.gif b/plugins/calendar/skins/default/images/loading-small.gif
new file mode 100644
index 00000000..d42f72c7
Binary files /dev/null and b/plugins/calendar/skins/default/images/loading-small.gif differ