Save changed attendee list.
}
break;
+ case 'GetFreeBusy':
+ $fb = Kronolith_FreeBusy::get(Horde_Util::getFormData('email'), true);
+ if ($fb instanceof PEAR_Error) {
+ $notification->push($fb->getMessage(), 'horde.warning');
+ break;
+ }
+ $result = new stdClass;
+ $result->fb = $fb;
+ break;
+
case 'SearchCalendars':
$result = new stdClass;
$result->events = 'Searched for calendars: ' . Horde_Util::getFormData('title');
$newAttendees = trim(Horde_Util::getFormData('newAttendees'));
$newResource = trim(Horde_Util::getFormData('resourceselect'));
- if (!empty($newAttendees)) {
- $parser = new Mail_RFC822;
- foreach (Horde_Mime_Address::explode($newAttendees) as $newAttendee) {
- // Parse the address without validation to see what we can get out of
- // it. We allow email addresses (john@example.com), email address with
- // user information (John Doe <john@example.com>), and plain names
- // (John Doe).
- $newAttendeeParsed = $parser->parseAddressList($newAttendee, '', false,
- false);
-
- // If we can't even get a mailbox out of the address, then it is
- // likely unuseable. Reject it entirely.
- if (is_a($newAttendeeParsed, 'PEAR_Error') ||
- !isset($newAttendeeParsed[0]) ||
- !isset($newAttendeeParsed[0]->mailbox)) {
- $notification->push(
- sprintf(_("Unable to recognize \"%s\" as an email address."),
- $newAttendee),
- 'horde.error');
- continue;
- }
-
- // Loop through any addresses we found.
- foreach ($newAttendeeParsed as $newAttendeeParsedPart) {
- // If there is only a mailbox part, then it is just a local name.
- if (empty($newAttendeeParsedPart->host)) {
- $attendees[] = array(
- 'attendance' => Kronolith::PART_REQUIRED,
- 'response' => Kronolith::RESPONSE_NONE,
- 'name' => $newAttendee,
- );
- continue;
- }
-
- // Build a full email address again and validate it.
- $name = empty($newAttendeeParsedPart->personal)
- ? ''
- : $newAttendeeParsedPart->personal;
-
- try {
- $newAttendeeParsedPartNew = Horde_Mime::encodeAddress(Horde_Mime_Address::writeAddress($newAttendeeParsedPart->mailbox, $newAttendeeParsedPart->host, $name));
- $newAttendeeParsedPartValidated = $parser->parseAddressList($newAttendeeParsedPartNew, '', null, true);
-
- $email = $newAttendeeParsedPart->mailbox . '@'
- . $newAttendeeParsedPart->host;
- // Avoid overwriting existing attendees with the default
- // values.
- if (!isset($attendees[$email]))
- $attendees[$email] = array(
- 'attendance' => Kronolith::PART_REQUIRED,
- 'response' => Kronolith::RESPONSE_NONE,
- 'name' => $name,
- );
- } catch (Horde_Mime_Exception $e) {
- $notification->push($e, 'horde.error');
- }
- }
- }
-
- $_SESSION['kronolith']['attendees'] = $attendees;
+ $newAttendees = Kronolith::parseAttendees($newAttendees);
+ if ($newAttendees) {
+ $_SESSION['kronolith']['attendees'] = $attendees + $newAttendees;
}
// Any new resources?
if (!empty($newResource)) {
-
/* Get the requested resource */
$resource = Kronolith::getDriver('Resource')->getResource($newResource);
/* Do our best to see what the response will be. Note that this response
- * is only guarenteed once the event is saved.
- */
+ * is only guarenteed once the event is saved. */
$date = new Horde_Date(Horde_Util::getFormData('date'));
$end = new Horde_Date(Horde_Util::getFormData('enddate'));
$response = $resource->getResponse(array('start' => $date, 'end' => $end));
KronolithCore = {
// Vars used and defaulting to null/false:
// DMenu, Growler, inAjaxCallback, is_logout, onDoActionComplete,
- // daySizes, viewLoading
+ // daySizes, viewLoading, freeBusy
view: '',
ecache: $H(),
efifo: {},
eventsLoading: {},
loading: 0,
+ fbLoading: 0,
date: new Date(),
tasktype: 'incomplete',
growls: 0,
}
/* Attendees */
+ this.freeBusy = $H();
+ $('kronolithEventStartDate').stopObserving('change');
if (!Object.isUndefined(ev.at)) {
$('kronolithEventAttendees').setValue(ev.at.pluck('l').join(', '));
var table = $('kronolithEventTabAttendees').down('tbody');
+ table.select('tr').invoke('remove');
ev.at.each(function(attendee) {
var tr = new Element('tr'), i;
+ this.fbLoading++;
+ this.doAction('GetFreeBusy',
+ { 'email': attendee.e },
+ function(r) {
+ this.fbLoading--;
+ if (!this.fbLoading) {
+ $('kronolithFBLoading').hide();
+ }
+ if (Object.isUndefined(r.response.fb)) {
+ return;
+ }
+ this.freeBusy.set(attendee.e, [ tr, r.response.fb ]);
+ this._insertFreeBusy(attendee.e);
+ }.bind(this));
tr.insert(new Element('td').writeAttribute('title', attendee.l).insert(attendee.e.escapeHTML()));
for (i = 0; i < 24; i++) {
- tr.insert(new Element('td'));
+ tr.insert(new Element('td', { 'class': 'kronolithFBUnknown' }));
}
table.insert(tr);
- });
+ }, this);
+ if (this.fbLoading) {
+ $('kronolithFBLoading').show();
+ }
+ $('kronolithEventStartDate').observe('change', function() {
+ ev.at.each(function(attendee) {
+ this._insertFreeBusy(attendee.e);
+ }, this);
+ }.bind(this));
}
/* Tags */
},
/**
+ * Inserts rows with free/busy information into the attendee table.
+ *
+ * @param string email An email address as the free/busy identifier.
+ */
+ _insertFreeBusy: function(email)
+ {
+ if (!$('kronolithEventDialog').visible() ||
+ !this.freeBusy.get(email)) {
+ return;
+ }
+ var fb = this.freeBusy.get(email)[1],
+ tr = this.freeBusy.get(email)[0],
+ td = tr.select('td')[1],
+ div = td.down('div');
+ if (!td.getWidth()) {
+ this._insertFreeBusy.bind(this, email).defer();
+ return;
+ }
+ tr.select('td').each(function(td, i) {
+ if (i != 0) {
+ td.addClassName('kronolithFBFree');
+ }
+ });
+ if (div) {
+ div.remove();
+ }
+ var start = Date.parseExact($F('kronolithEventStartDate'), Kronolith.conf.date_format),
+ end = start.clone().add(1).days(),
+ width = td.getWidth();
+ div = new Element('div').setStyle({ 'position': 'relative' });
+ td.insert(div);
+ $H(fb.b).each(function(busy) {
+ var from = new Date(), to = new Date(), left;
+ from.setTime(busy.key * 1000);
+ to.setTime(busy.value * 1000);
+ if (from.isAfter(end) || to.isBefore(start)) {
+ return;
+ }
+ if (from.isBefore(start)) {
+ from = start.clone();
+ }
+ if (to.isAfter(end)) {
+ to = end.clone();
+ }
+ if (to.getHours() == 0 && to.getMinutes() == 0) {
+ to.add(-1).minutes();
+ }
+ left = from.getHours() + from.getMinutes() / 60;
+ div.insert(new Element('div', { 'class': 'kronolithFBBusy' }).setStyle({ 'zIndex': 1, 'top': 0, 'left': (left * width) + 'px', 'width': (((to.getHours() + to.getMinutes() / 60) - left) * width) + 'px' }));
+ });
+ },
+
+ /**
* Toggles the start and end time fields of the event edit form on and off.
*
* @param boolean on Whether the event is an all-day event, i.e. the time
if (isset($_SESSION['kronolith']['attendees']) && is_array($_SESSION['kronolith']['attendees'])) {
$this->setAttendees($_SESSION['kronolith']['attendees']);
}
+ if ($attendees = Horde_Util::getFormData('attendees')) {
+ $attendees = Kronolith::parseAttendees(trim($attendees));
+ if ($attendees) {
+ $this->setAttendees($attendees);
+ }
+ }
// Resources
if (isset($_SESSION['kronolith']['resources']) && is_array($_SESSION['kronolith']['resources'])) {
* information is available.
*
* @param string $email The email address to look for.
+ * @param boolean $json Whether to return the free/busy data as a simple
+ * object suitable to be transferred as json.
*
* @return Horde_iCalendar_vfreebusy Free/busy component on success,
- * PEAR_Error on failure
+ * PEAR_Error on failure.
*/
- function get($email)
+ function get($email, $json = false)
{
/* Properly handle RFC822-compliant email addresses. */
static $rfc822;
}
if ($found) {
- return $vFb;
+ return $json ? Kronolith_FreeBusy::toJson($vFb) : $vFb;
}
}
}
$fb = $storage->search($email);
if (!is_a($fb, 'PEAR_Error')) {
- return $fb;
+ return $json ? Kronolith_FreeBusy::toJson($fb) : $fb;
} elseif ($fb->getCode() == Kronolith::ERROR_FB_NOT_FOUND) {
return $url ?
PEAR::raiseError(sprintf(_("No free/busy information found at the free/busy url of %s."), $email)) :
$vFb = Horde_iCalendar::newComponent('vfreebusy', $vCal);
$vFb->setAttribute('ORGANIZER', $email);
- return $vFb;
+ return $json ? Kronolith_FreeBusy::toJson($vFb) : $vFb;
}
/**
return $result;
}
+ /**
+ * Converts free/busy data to a simple object suitable to be transferred
+ * as json.
+ *
+ * @param Horde_iCalendar_vfreebusy $fb A Free/busy component.
+ *
+ * @return object A simple object representation.
+ */
+ function toJson($fb)
+ {
+ $json = new stdClass;
+ $json->e = $fb->getEmail();
+ $start = $fb->getStart();
+ if ($start) {
+ $start = new Horde_Date($start);
+ $json->s = $start->dateString();
+ }
+ $end = $fb->getStart();
+ if ($end) {
+ $end = new Horde_Date($end);
+ $json->e = $end->dateString();
+ }
+ $json->b = $fb->getBusyPeriods();
+ return $json;
+ }
+
}
}
/**
+ * Parses a comma separated list of names and e-mail addresses into a list
+ * of attendee hashes.
+ *
+ * @param string $newAttendees A comma separated attendee list.
+ *
+ * @return array The attendee list with e-mail addresses as keys and
+ * attendee information as values.
+ */
+ public static function parseAttendees($newAttendees)
+ {
+ if (empty($newAttendees)) {
+ return;
+ }
+
+ $parser = new Mail_RFC822;
+ $attendees = array();
+ foreach (Horde_Mime_Address::explode($newAttendees) as $newAttendee) {
+ // Parse the address without validation to see what we can get out
+ // of it. We allow email addresses (john@example.com), email
+ // address with user information (John Doe <john@example.com>),
+ // and plain names (John Doe).
+ $newAttendeeParsed = $parser->parseAddressList($newAttendee, '',
+ false, false);
+
+ // If we can't even get a mailbox out of the address, then it is
+ // likely unuseable. Reject it entirely.
+ if (is_a($newAttendeeParsed, 'PEAR_Error') ||
+ !isset($newAttendeeParsed[0]) ||
+ !isset($newAttendeeParsed[0]->mailbox)) {
+ $notification->push(
+ sprintf(_("Unable to recognize \"%s\" as an email address."),
+ $newAttendee),
+ 'horde.error');
+ continue;
+ }
+
+ // Loop through any addresses we found.
+ foreach ($newAttendeeParsed as $newAttendeeParsedPart) {
+ // If there is only a mailbox part, then it is just a local
+ // name.
+ if (empty($newAttendeeParsedPart->host)) {
+ $attendees[] = array(
+ 'attendance' => Kronolith::PART_REQUIRED,
+ 'response' => Kronolith::RESPONSE_NONE,
+ 'name' => $newAttendee,
+ );
+ continue;
+ }
+
+ // Build a full email address again and validate it.
+ $name = empty($newAttendeeParsedPart->personal)
+ ? ''
+ : $newAttendeeParsedPart->personal;
+
+ try {
+ $newAttendeeParsedPartNew = Horde_Mime::encodeAddress(Horde_Mime_Address::writeAddress($newAttendeeParsedPart->mailbox, $newAttendeeParsedPart->host, $name));
+ $newAttendeeParsedPartValidated = $parser->parseAddressList($newAttendeeParsedPartNew, '', null, true);
+
+ $email = $newAttendeeParsedPart->mailbox . '@'
+ . $newAttendeeParsedPart->host;
+ // Avoid overwriting existing attendees with the default
+ // values.
+ $attendees[$email] = array(
+ 'attendance' => Kronolith::PART_REQUIRED,
+ 'response' => Kronolith::RESPONSE_NONE,
+ 'name' => $name);
+ } catch (Horde_Mime_Exception $e) {
+ $notification->push($e, 'horde.error');
+ }
+ }
+ }
+
+ return $attendees;
+ }
+
+ /**
* Returns a comma separated list of attendees and resources
*
* @return string Attendee/Resource list.
</div>
<div id="kronolithEventTabAttendees" class="kronolithTabsOption" style="display:none">
- <input type="text" name="participants" id="kronolithEventAttendees" class="kronolithLongField" value="" /><br />
+ <input type="text" name="attendees" id="kronolithEventAttendees" class="kronolithLongField" value="" /><br />
<label><input type="checkbox" name="sendupdates" value="" /> <?php printf(_("send invites %s to all attendees"), '</label>') ?><br />
+ <div id="kronolithFBLoading" style="display:none"></div>
<table width="100%" cellspacing="0" cellpadding="0" border="0">
<thead>
<tr>
background: #28b22b;
color: #fff;
}
+.kronolithAjax .kronolithFBFree {
+ position: relative;
+}
+.kronolithAjax .kronolithFBBusy {
+ position: absolute;
+}
div.fbgrid {
background-color: #fff;
overflow: auto;
top: -3px;
vertical-align: middle;
}
-#kronolithLoading {
+#kronolithLoading, #kronolithFBLoading {
background: transparent url("graphics/loading.gif") no-repeat center;
padding: 2px;
width: 16px;
height: 16px;
border: 1px #c0c0c0 solid;
}
+#kronolithFBLoading {
+ position: absolute;
+ top: 65px;
+ border: none;
+}
/* User data and options */
#kronolithServices {
}
.kronolithTabsOption {
+ position: relative;
line-height: 250%;
}
#kronolithEventTabAttendees table {
#kronolithEventTabAttendees th.night {
background-color: #ccc;
}
+#kronolithEventTabAttendees td div {
+ margin: 0;
+ height: 100%;
+}
/* Mini calendar */
.kronolithMinical {