Render and update free/busy information in the attendees list.
authorJan Schneider <jan@horde.org>
Tue, 24 Nov 2009 15:35:27 +0000 (16:35 +0100)
committerJan Schneider <jan@horde.org>
Tue, 24 Nov 2009 15:36:26 +0000 (16:36 +0100)
Save changed attendee list.

kronolith/ajax.php
kronolith/attendees.php
kronolith/js/kronolith.js
kronolith/lib/Event.php
kronolith/lib/FreeBusy.php
kronolith/lib/Kronolith.php
kronolith/templates/index/edit.inc
kronolith/themes/screen.css

index df4887c..c8e7f53 100644 (file)
@@ -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');
index 5245e6e..4e251a2 100644 (file)
@@ -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 <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));
index 11ae0d5..5881cb7 100644 (file)
@@ -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
index b651fcd..add0f8a 100644 (file)
@@ -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'])) {
index 0f30d7e..d0b223b 100644 (file)
@@ -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;
+    }
+
 }
index 89c30bc..130e6d1 100644 (file)
@@ -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 <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.
index 8556e5b..c8dd038 100644 (file)
 </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>
index 33890bb..6b0a4fd 100644 (file)
@@ -185,6 +185,12 @@ a.newEvent img {
     background: #28b22b;
     color: #fff;
 }
+.kronolithAjax .kronolithFBFree {
+    position: relative;
+}
+.kronolithAjax .kronolithFBBusy {
+    position: absolute;
+}
 div.fbgrid {
     background-color: #fff;
     overflow: auto;
@@ -374,13 +380,18 @@ body.kronolithAjax {
     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 {
@@ -691,6 +702,7 @@ div#kronolithEventTabTags {
 }
 
 .kronolithTabsOption {
+    position: relative;
     line-height: 250%;
 }
 #kronolithEventTabAttendees table {
@@ -717,6 +729,10 @@ div#kronolithEventTabTags {
 #kronolithEventTabAttendees th.night {
     background-color: #ccc;
 }
+#kronolithEventTabAttendees td div {
+    margin: 0;
+    height: 100%;
+}
 
 /* Mini calendar */
 .kronolithMinical {