From 05cdfe4b0bbb6ad7b501bf7743ec58d3855392b6 Mon Sep 17 00:00:00 2001 From: Jan Schneider Date: Tue, 24 Nov 2009 16:35:27 +0100 Subject: [PATCH] Render and update free/busy information in the attendees list. Save changed attendee list. --- kronolith/ajax.php | 10 +++++ kronolith/attendees.php | 67 ++---------------------------- kronolith/js/kronolith.js | 85 ++++++++++++++++++++++++++++++++++++-- kronolith/lib/Event.php | 6 +++ kronolith/lib/FreeBusy.php | 38 ++++++++++++++--- kronolith/lib/Kronolith.php | 76 ++++++++++++++++++++++++++++++++++ kronolith/templates/index/edit.inc | 3 +- kronolith/themes/screen.css | 18 +++++++- 8 files changed, 230 insertions(+), 73 deletions(-) diff --git a/kronolith/ajax.php b/kronolith/ajax.php index df4887ce7..c8e7f53c7 100644 --- a/kronolith/ajax.php +++ b/kronolith/ajax.php @@ -454,6 +454,16 @@ try { } 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'); diff --git a/kronolith/attendees.php b/kronolith/attendees.php index 5245e6ec8..4e251a259 100644 --- a/kronolith/attendees.php +++ b/kronolith/attendees.php @@ -36,77 +36,18 @@ case 'add': $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 ), 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)); diff --git a/kronolith/js/kronolith.js b/kronolith/js/kronolith.js index 11ae0d51c..5881cb7a1 100644 --- a/kronolith/js/kronolith.js +++ b/kronolith/js/kronolith.js @@ -17,7 +17,7 @@ var frames = { horde_main: true }, KronolithCore = { // Vars used and defaulting to null/false: // DMenu, Growler, inAjaxCallback, is_logout, onDoActionComplete, - // daySizes, viewLoading + // daySizes, viewLoading, freeBusy view: '', ecache: $H(), @@ -25,6 +25,7 @@ KronolithCore = { efifo: {}, eventsLoading: {}, loading: 0, + fbLoading: 0, date: new Date(), tasktype: 'incomplete', growls: 0, @@ -2917,17 +2918,42 @@ KronolithCore = { } /* 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 */ @@ -2951,6 +2977,59 @@ KronolithCore = { }, /** + * 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 diff --git a/kronolith/lib/Event.php b/kronolith/lib/Event.php index b651fcd05..add0f8a63 100644 --- a/kronolith/lib/Event.php +++ b/kronolith/lib/Event.php @@ -1756,6 +1756,12 @@ abstract class Kronolith_Event 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'])) { diff --git a/kronolith/lib/FreeBusy.php b/kronolith/lib/FreeBusy.php index 0f30d7e9c..d0b223be1 100644 --- a/kronolith/lib/FreeBusy.php +++ b/kronolith/lib/FreeBusy.php @@ -124,11 +124,13 @@ class Kronolith_FreeBusy { * 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; @@ -191,7 +193,7 @@ class Kronolith_FreeBusy { } if ($found) { - return $vFb; + return $json ? Kronolith_FreeBusy::toJson($vFb) : $vFb; } } } @@ -201,7 +203,7 @@ class Kronolith_FreeBusy { $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)) : @@ -213,7 +215,7 @@ class Kronolith_FreeBusy { $vFb = Horde_iCalendar::newComponent('vfreebusy', $vCal); $vFb->setAttribute('ORGANIZER', $email); - return $vFb; + return $json ? Kronolith_FreeBusy::toJson($vFb) : $vFb; } /** @@ -241,4 +243,30 @@ class Kronolith_FreeBusy { 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; + } + } diff --git a/kronolith/lib/Kronolith.php b/kronolith/lib/Kronolith.php index 89c30bcf6..130e6d1bb 100644 --- a/kronolith/lib/Kronolith.php +++ b/kronolith/lib/Kronolith.php @@ -1334,6 +1334,82 @@ class Kronolith } /** + * 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 ), + // 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. diff --git a/kronolith/templates/index/edit.inc b/kronolith/templates/index/edit.inc index 8556e5be2..c8dd03868 100644 --- a/kronolith/templates/index/edit.inc +++ b/kronolith/templates/index/edit.inc @@ -142,8 +142,9 @@