Merge branch '2015.11' into 2015.11-develop
authorPhilipp Schüle <p.schuele@metaways.de>
Wed, 16 Mar 2016 16:26:56 +0000 (17:26 +0100)
committerPhilipp Schüle <p.schuele@metaways.de>
Wed, 16 Mar 2016 16:26:56 +0000 (17:26 +0100)
1  2 
tests/tine20/Crm/NotificationsTests.php
tine20/Addressbook/Setup/Initialize.php
tine20/Calendar/js/DaysView.js
tine20/Calendar/js/MainScreenCenterPanel.js
tine20/Crm/Controller/Lead.php
tine20/Setup/Controller.php
tine20/Tinebase/Container.php

@@@ -38,6 -38,8 +38,8 @@@ class Crm_NotificationsTests extends Cr
      
      /**
       * testNotification
+      *
+      * @return Crm_Model_Lead
       */
      public function testNotification()
      {
              'related_record'         => $this->_getContact(),
              'own_model'              => 'Crm_Model_Lead',
              'own_backend'            => 'Sql',
 -            'own_degree'             => Tinebase_Model_Relation::DEGREE_SIBLING,
 +            'related_degree'         => Tinebase_Model_Relation::DEGREE_SIBLING,
              'related_model'          => 'Addressbook_Model_Contact',
              'related_backend'        => Tasks_Backend_Factory::SQL,
          ), TRUE));
-         $this->_leadController->create($lead);
+         $savedLead = $this->_leadController->create($lead);
          
          $messages = self::getMessages();
          $this->assertEquals(1, count($messages));
          $bodyText = $messages[0]->getBodyText()->getContent();
          $this->assertContains(' Lars Kneschke (Metaways', $bodyText);
+         return $savedLead;
      }
  
      /**
              'related_record'         => Addressbook_Controller_Contact::getInstance()->getContactByUserId(Tinebase_Core::getUser()->getId()),
              'own_model'              => 'Crm_Model_Lead',
              'own_backend'            => 'Sql',
 -            'own_degree'             => Tinebase_Model_Relation::DEGREE_SIBLING,
 +            'related_degree'         => Tinebase_Model_Relation::DEGREE_SIBLING,
              'related_model'          => 'Addressbook_Model_Contact',
              'related_backend'        => Tasks_Backend_Factory::SQL,
          ), TRUE));
-         $savedLead = $this->_leadController->create($lead);
+         $this->_leadController->create($lead);
          
          $messages = self::getMessages();
          $this->assertEquals(1, count($messages));
                  'related_record'         => Addressbook_Controller_Contact::getInstance()->getContactByUserId(Tinebase_Core::getUser()->getId()),
                  'own_model'              => 'Crm_Model_Lead',
                  'own_backend'            => 'Sql',
 -                'own_degree'             => Tinebase_Model_Relation::DEGREE_SIBLING,
 +                'related_degree'         => Tinebase_Model_Relation::DEGREE_SIBLING,
                  'related_model'          => 'Addressbook_Model_Contact',
                  'related_backend'        => Tasks_Backend_Factory::SQL,
          ), TRUE));
          $bodyText = $messages[0]->getBodyText()->getContent();
          $this->assertContains('**PHPUnit **', $bodyText);
      }
+     /**
+      * @see 0011694: show tags and history / latest changes in lead notification mail
+      */
+     public function testTagAndHistory()
+     {
+         $lead = $this->testNotification();
+         self::flushMailer();
+         $tag = new Tinebase_Model_Tag(array(
+             'type'  => Tinebase_Model_Tag::TYPE_SHARED,
+             'name'  => 'testNotificationTag',
+             'description' => 'testNotificationTag',
+             'color' => '#009B31',
+         ));
+         $lead->tags = array($tag);
+         $lead->description = 'updated description';
+         $this->_leadController->update($lead);
+         $messages = self::getMessages();
+         $this->assertEquals(1, count($messages));
+         $bodyText = quoted_printable_decode($messages[0]->getBodyText()->getContent());
+         $translate = Tinebase_Translation::getTranslation('Crm');
+         $changeMessage = $translate->_("'%s' changed from '%s' to '%s'.");
+         $this->assertContains("testNotificationTag\n", $bodyText);
+         $this->assertContains(sprintf($changeMessage, 'description', 'Description', 'updated description'), $bodyText);
+     }
  }
@@@ -166,7 -166,7 +166,7 @@@ class Addressbook_Setup_Initialize exte
          $adminGroup = $groupsBackend->getDefaultAdminGroup();
          
          // give anyone read rights to the internal addressbook
 -        // give Adminstrators group read/edit/admin rights to the internal addressbook
 +        // give Administrators group read/edit/admin rights to the internal addressbook
          Tinebase_Container::getInstance()->addGrants($this->_getInternalAddressbook(), Tinebase_Acl_Rights::ACCOUNT_TYPE_ANYONE, '0', array(
              Tinebase_Model_Grants::GRANT_READ
          ), TRUE);
                      'adminPassword' => $loginConfig->password,
                  );
              } else {
-                 throw Setup_Exception('Inital admin username and password are required');
+                 throw new Setup_Exception('Inital admin username and password are required');
              }
          }
          
@@@ -70,15 -70,7 +70,15 @@@ Tine.Calendar.DaysView = function(confi
           * 
           * @param {Tine.Calendar.Model.Event} event
           */
 -        'updateEvent'
 +        'updateEvent',
 +        /**
 +         * @event onBeforeAllDayScrollerResize
 +         * fired when an the allDayArea gets resized
 +         *
 +         * @param {Tine.Calendar.Model.DaysView} this
 +         * @param {number} heigt
 +         */
 +        'onBeforeAllDayScrollerResize'
      );
  };
  
@@@ -135,45 -127,37 +135,45 @@@ Ext.extend(Tine.Calendar.DaysView, Ext.
      timeGranularity: 30,
      /**
       * @cfg {Number} timeIncrement
 -     * time increment for range adds/edits
 +     * time increment for range adds/edits (minutes)
       */
      timeIncrement: 15,
      /**
 -     * @cfg {Number} granularityUnitHeights
 -     * heights in px of a granularity unit
 +     * @cfg {Number} timeVisible
 +     * time visible in scrolling area (minutes)
       */
 -    granularityUnitHeights: 18,
 +    timeVisible: '10:00',
      /**
       * @cfg {Boolean} denyDragOnMissingEditGrant
       * deny drag action if edit grant for event is missing
       */
      denyDragOnMissingEditGrant: true,
 +
 +
      /**
       * store holding timescale
 -     * @type {Ext.data.Store}
 +     * @property {Ext.data.Store}
 +     * @private
       */
      timeScale: null,
      /**
       * The amount of space to reserve for the scrollbar (defaults to 19 pixels)
 -     * @type {Number}
 +     * @property {Number}
 +     * @private
       */
      scrollOffset: 19,
 -
      /**
       * The time in milliseconds, a scroll should be delayed after using the mousewheel
 -     * 
 -     * @type Number
 +     * @property Number
 +     * @private
       */
      scrollBuffer: 200,
 -    
 +    /**
 +     * The minmum all day height in px
 +     * @property Number
 +     * @private
 +     */
 +    minAllDayScrollerHight: 10,
      /**
       * @property {bool} editing
       * @private
          this.initTimeScale();
          this.initTemplates();
          
 -        this.on('beforehide', this.onBeforeHide, this);
 -        this.on('show', this.onShow, this);
 -        
          this.mon(Tine.Tinebase.appMgr, 'activate', this.onAppActivate, this);
          
          if (! this.selModel) {
          var prefs = this.app.getRegistry().get('preferences'),
              defaultStartTime = Date.parseDate(prefs.get('daysviewdefaultstarttime'), 'H:i'),
              startTime = Date.parseDate(prefs.get('daysviewstarttime'), 'H:i'),
 -            endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i');
 -        
 +            endTime = Date.parseDate(prefs.get('daysviewendtime'), 'H:i'),
 +            timeVisible = Date.parseTimePart(prefs.get('daysviewtimevisible'), 'H:i');
 +
          this.dayStart = Ext.isDate(startTime) ? startTime : Date.parseDate(this.dayStart, 'H:i');
          this.dayEnd = Ext.isDate(endTime) ? endTime : Date.parseDate(this.dayEnd, 'H:i');
          // 00:00 in users timezone is a spechial case where the user expects
          if (this.dayEnd.format('H:i') == '00:00') {
              this.dayEnd = this.dayEnd.add(Date.MINUTE, -1);
          }
 -        this.dayEndPx = this.getTimeOffset(this.dayEnd);
 -        
 -        this.cropDayTime = !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar') && !(!this.getTimeOffset(this.dayStart) && !this.getTimeOffset(this.dayEnd));
 +
 +        this.timeVisible = Ext.isDate(timeVisible) ? timeVisible : Date.parseTimePart(this.timeVisible, 'H:i');
 +
 +        this.cropDayTime = !! Tine.Tinebase.configManager.get('daysviewcroptime', 'Calendar');
  
          if (this.cropDayTime) {
              this.defaultStart = Ext.isDate(defaultStartTime) ? defaultStartTime : Date.parseDate(this.defaultStart, 'H:i');
  
                  sourceEl.setStyle({'border-style': 'dashed'});
                  sourceEl.setOpacity(0.5);
 +
 +                data.denyDrop = true;
                  
                  if (data.event) {
                      var event = data.event;
                          }
  
                          if (event.get('editGrant')) {
 -                            return this.view == sourceView && Math.abs(targetDateTime.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE ? 'cal-daysviewpanel-event-drop-nodrop' : 'cal-daysviewpanel-event-drop-ok';
 +                            data.denyDrop = this.view == sourceView && Math.abs(targetDateTime.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE;
 +
 +                            if (data.dtstartLimit && targetDateTime.getTimePart() > data.dtstartLimit) {
 +                                data.denyDrop = true;
 +                            }
 +
 +                            return  data.denyDrop ? 'cal-daysviewpanel-event-drop-nodrop' : 'cal-daysviewpanel-event-drop-ok';
                          }
                      }
                  }
                      var event = data.event,
                          originalDuration = (event.get('dtend').getTime() - event.get('dtstart').getTime()) / Date.msMINUTE;
  
 -                    // Get the new endDate to ensure it's not out of croptimes
 -                    var outOfCropBounds = false;
 -                    if (v.cropDayTime == true) {
 -                        var newEnd = new Date(),
 -                            dayEndCompare = new Date();
 -
 -                        newEnd.setTime(targetDate.getTime());
 -                        newEnd.add(Date.MINUTE, originalDuration);
 -                        dayEndCompare.setTime(targetDate.getTime());
 -                        dayEndCompare.setHours(v.dayEnd.getHours(), v.dayEnd.getMinutes());
 -                        outOfCropBounds = (newEnd > dayEndCompare);
 -                    }
 -
 -                    // deny drop for missing edit grant or no time change
 -                    if (! event.get('editGrant') || (v == this.view && Math.abs(targetDate.getTime() - event.get('dtstart').getTime()) < Date.msMINUTE)
 -                            || outOfCropBounds) {
 +                    if (data.denyDrop) {
                          return false;
                      }
  
                      if (this.view.ownerCt.attendee) {
                          var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(event.get('attendee')),
                              sourceAttendee = Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, event.view.ownerCt.attendee),
+                             destinationAttendee = Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, this.view.ownerCt.attendee);
+                         if (! destinationAttendee) {
                              destinationAttendee = new Tine.Calendar.Model.Attender(this.view.ownerCt.attendee.data);
  
-                         attendeeStore.remove(sourceAttendee);
-                         attendeeStore.add(destinationAttendee);
+                             attendeeStore.remove(sourceAttendee);
+                             attendeeStore.add(destinationAttendee);
  
-                         Tine.Calendar.Model.Attender.getAttendeeStore.getData(attendeeStore, event);
+                             Tine.Calendar.Model.Attender.getAttendeeStore.getData(attendeeStore, event);
+                         }
                      }
  
                      event.endEdit();
       */
      initDragZone: function() {
          this.scroller.ddScrollConfig = {
 -            vthresh: this.granularityUnitHeights * 2,
 -            increment: this.granularityUnitHeights * 4,
              hthresh: -1,
              frequency: 500
          };
              containerScroll: true,
              
              getDragData: function(e) {
 +                // adjust scrollConfig
 +                var scrollUnit = this.view.getTimeOffset(this.view.timeIncrement);
 +                this.view.scroller.ddScrollConfig.vthresh = scrollUnit *2;
 +                this.view.scroller.ddScrollConfig.increment =  scrollUnit * 4;
 +
                  var selected = this.view.getSelectionModel().getSelectedEvents();
                  
                  var eventEl = e.getTarget('div.cal-daysviewpanel-event', 10);
                      }
                      
                      // we need to clone an event with summary in
 -                    var d = Ext.get(event.ui.domIds[0]).dom.cloneNode(true);
 +                    var eventEl = Ext.get(event.ui.domIds[0]),
 +                        eventBox = eventEl.getBox(),
 +                        d = eventEl.dom.cloneNode(true);
 +
                      d.id = Ext.id();
 -                    
 +
                      if (event.get('is_all_day_event')) {
 +                        Ext.fly(d).setTop(-2);
                          Ext.fly(d).setLeft(0);
 +                        Ext.fly(d).setWidth(eventBox.width);
                      } else {
                          var width = (Ext.fly(this.view.dayCols[0]).getWidth() * 0.9);
                          Ext.fly(d).setTop(0);
                          Ext.fly(d).setWidth(width);
 -                        Ext.fly(d).setHeight(this.view.getTimeHeight.call(this.view, event.get('dtstart'), event.get('dtend')));
 +                        Ext.fly(d).setHeight(eventBox.height);
                      }
  
                      return {
                          sourceEl: eventEl,
                          event: event,
                          ddel: d,
 -                        selections: this.view.getSelectionModel().getSelectedEvents()
 +                        selections: this.view.getSelectionModel().getSelectedEvents(),
 +                        dtstartLimit: this.view.cropDayTime ? this.view.dayEnd.getTimePart() - event.duration : false
                      }
                  }
              },
          this.initDragZone();
          
          this.updatePeriod({from: this.startDate});
 -        
 -        if (this.store.getCount()) {
 -            this.onLoad.apply(this);
 -        }
 -        
 +
          // apply os specific scrolling space
          Ext.fly(this.innerHd.firstChild.firstChild).setStyle('margin-right', Ext.getScrollBarWidth() + 'px');
          
          // crop daytime
          if (this.cropDayTime) {
 -            var cropStartPx = this.getTimeOffset(this.dayStart),
 -                cropHeightPx = this.getTimeOffset(this.dayEnd) +2;
 -                
 -            this.mainBody.setStyle('margin-top', '-' + cropStartPx + 'px');
 -            this.mainBody.setStyle('height', cropHeightPx + 'px');
 -            this.mainBody.setStyle('overflow', 'hidden');
 +            this.cropper.setStyle('overflow', 'hidden');
              this.scroller.addClass('cal-daysviewpanel-body-cropDayTime');
          }
  
 -        this.unbufferedOnLayout();
 +        if (this.store.getCount()) {
 +            this.onLoad.defer(100, this);
 +        }
  
          // scrollTo initial position
          this.isScrolling = true;
      onBeforeScroll: function() {
          if (! this.isScrolling) {
              this.isScrolling = true;
 -            
 +
 +            var topOffset = this.getHeightMinutes(this.scroller.dom.scrollTop);
 +            if (topOffset) {
 +                this.lastScrollTime = this.dayStart.clearTime(true).add(Date.MINUTE, topOffset);
 +            }
 +
              // walk all cols an hide hints
              Ext.each(this.dayCols, function(dayCol, idx) {
                  this.aboveHints.item(idx).setDisplayed(false);
       * @param {} o
       */
      onScroll: function(e, t, o) {
 +        // no arguments means programatic scroll (show/hide/...)
 +        if (! arguments.length) {
 +            var topTime = this.lastScrollTime || this.defaultStart,
 +                topOffset = this.getTimeOffset(topTime);
 +
 +            if (topOffset) {
 +                this.scroller.dom.scrollTop = this.getTimeOffset(topTime);
 +            }
 +        }
 +
          var visibleHeight = this.scroller.dom.clientHeight,
              visibleStart  = this.scroller.dom.scrollTop - this.mainBody.dom.offsetTop,
              visibleEnd    = visibleStart + visibleHeight,
              vStartMinutes = this.getHeightMinutes(visibleStart),
              vEndMinutes   = this.getHeightMinutes(visibleEnd);
 -            
 +
          Ext.each(this.dayCols, function(dayCol, idx) {
              var dayColEl    = Ext.get(dayCol),
                  dayStart    = this.startDate.add(Date.DAY, idx),
                  aboveEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart, dayStart.add(Date.MINUTE, vStartMinutes)),
                  belowEvents = this.parallelScrollerEventsRegistry.getEvents(dayStart.add(Date.MINUTE, vEndMinutes), dayStart.add(Date.DAY, 1));
 -                
 +
              if (aboveEvents.length) {
                  var aboveHint = this.aboveHints.item(idx);
                  aboveHint.setTop(visibleStart + 5);
                      aboveHint.fadeIn({duration: 1.6});
                  }
              }
 -            
 +
              if (belowEvents.length) {
                  var belowHint = this.belowHints.item(idx);
                  belowHint.setTop(visibleEnd - 14);
                  }
              }
          }, this);
 -        
 +
          this.isScrolling = false;
      },
 -    
 -    onShow: function() {
 -        this.onLayout();
 -        this.scroller.dom.scrollTop = this.lastScrollPos || this.getTimeOffset(this.defaultStart);
 -    },
 -    
 -    onBeforeHide: function() {
 -        this.lastScrollPos = this.scroller.dom.scrollTop;
 -    },
 -    
 +
      /**
       * renders a single event into this daysview
       * @param {Tine.Calendar.Model.Event} event
          
          this.updateDayHeaders();
          this.redrawWholeDayEvents.defer(50, this);
 +
 +        this.unbufferedOnLayout();
      },
      
      redrawWholeDayEvents: function() {
          var sm = this.getSelectionModel();
          sm.select(targetEvent);
          
 -        var dtStart = this.getTargetDateTime(e);
 +        var dtStart = this.getTargetDateTime(e),
 +            dtEnd = this.getTargetDateTime(e, this.timeIncrement, 's');
 +
          if (dtStart) {
              var newId = 'cal-daysviewpanel-new-' + Ext.id();
              var event = new Tine.Calendar.Model.Event(Ext.apply(Tine.Calendar.Model.Event.getDefaultData(), {
                  id: newId,
                  dtstart: dtStart,
 -                dtend: dtStart.is_all_day_event ? dtStart.add(Date.HOUR, 24).add(Date.SECOND, -1) : dtStart.add(Date.MINUTE, Tine.Calendar.Model.Event.getMeta('defaultEventDuration')),
 +                dtend: dtStart.is_all_day_event ? dtStart.add(Date.HOUR, 24).add(Date.SECOND, -1) : dtEnd,
                  is_all_day_event: dtStart.is_all_day_event
              }), newId);
              event.isRangeAdd = true;
      onBeforeEventResize: function(rz, e) {
          var parts = rz.el.id.split(':');
          var event = this.store.getById(parts[1]);
 -        
 +
 +        // @TODO compute max minutes also
 +        var maxHeight = 10000;
 +
 +        if (this.cropDayTime) {
 +            var maxMinutes = (this.dayEnd.getTimePart() - event.get('dtstart').getTimePart()) / Date.msMINUTE;
 +            maxHeight = this.getTimeOffset(maxMinutes);
 +        }
 +
 +        rz.heightIncrement = this.getTimeOffset(this.timeIncrement);
 +        rz.maxHeight = maxHeight,
 +
          rz.event = event;
          rz.originalHeight = rz.el.getHeight();
          rz.originalWidth  = rz.el.getWidth();
       * @private
       */
      onAdd : function(ds, records, index){
 -        //console.log('onAdd');
          for (var i=0; i<records.length; i++) {
              var event = records[i];
              
       * get date of a (event) target
       * 
       * @param {Ext.EventObject} e
 +     * @param {number} graticule
 +     * @param {char} graticuleEdge one of n (north) s, (south)
       * @return {Date}
       */
 -    getTargetDateTime: function(e) {
 +    getTargetDateTime: function(e, graticule, graticuleHandle) {
          var target = e.getTarget('div[class^=cal-daysviewpanel-datetime]');
          
          if (target && target.id.match(/^ext-gen\d+:\d+/)) {
              date.is_all_day_event = true;
              
              if (parts[2] ) {
 -                var timePart = this.timeScale.getAt(parts[2]);
 -                date = date.add(Date.MINUTE, timePart.get('minutes'));
 +                var timePart = this.timeScale.getAt(parts[2]),
 +                    eventXY = e.getXY(),
 +                    mainBodyXY = this.mainBody.getXY(),
 +                    offsetPx = eventXY[1] - mainBodyXY[1],
 +                    offsetMinutes = this.getHeightMinutes(offsetPx),
 +                    graticule = graticule ? graticule : this.timeGranularity,
 +                    graticuleHandle = graticuleHandle ? graticuleHandle : 'n',
 +                    graticuleFn = graticuleHandle == 'n' ? Math.floor : Math.ceil;
 +
 +                // constraint to graticule
 +                offsetMinutes = graticuleFn(offsetMinutes/graticule) * graticule;
 +
 +                date = date.add(Date.MINUTE, offsetMinutes);
                  date.is_all_day_event = false;
              }
              
              return this.store.getById(parts[1]);
          }
      },
 -    
 +
 +    /**
 +     * get offset in px for the time part of given date (with current scaling)
 +     *
 +     * @param {date} date
 +     * @returns {number}
 +     */
      getTimeOffset: function(date) {
 -        var d = this.granularityUnitHeights / this.timeGranularity;
 -        
 -        return Math.round(d * ( 60 * date.getHours() + date.getMinutes()));
 +        if (this.mainBody) {
 +            var minutes = Ext.isDate(date) ? date.getTimePart() / Date.msMINUTE : date,
 +                d = this.mainBody.getHeight() / (24 * 60);
 +
 +            return Math.round(d * minutes);
 +        }
      },
 -    
 +
 +    /**
 +     * get offset in % for the time part of given date
 +     *
 +     * @param {date|number} date
 +     * @returns {number}
 +     */
 +    getTimeOffsetPct: function(date) {
 +        var minutes = Ext.isDate(date) ? date.getTimePart() / Date.msMINUTE : date;
 +
 +        return 100 * ((Date.msMINUTE * minutes) / Date.msDAY);
 +    },
 +
 +    /**
 +     * get height in px of the diff for the given dates (with current scaling)
 +     *
 +     * @param {date} dtStart
 +     * @param {date} dtEnd
 +     * @returns {number}
 +     */
      getTimeHeight: function(dtStart, dtEnd) {
 -        var d = this.granularityUnitHeights / this.timeGranularity;
 -        return Math.round(d * ((dtEnd.getTime() - dtStart.getTime()) / Date.msMINUTE));
 +        if (this.mainBody) {
 +            var d = this.mainBody.getHeight() / (24 * 60);
 +            return Math.round(d * ((dtEnd.getTime() - dtStart.getTime()) / Date.msMINUTE));
 +        }
      },
 -    
 +
 +    /**
 +     * get height in % of the diff for the given dates
 +     *
 +     * @param {date} dtStart
 +     * @param {date} dtEnd
 +     * @returns {number}
 +     */
 +    getTimeHeightPct: function(dtStart, dtEnd) {
 +        return 100 * ((dtEnd.getTime() - dtStart.getTime()) / Date.msDAY);
 +    },
 +
 +    /**
 +     * get number of minutes represented by height in px (current scaleing)
 +     *
 +     * @param {number} height
 +     * @returns {number}
 +     */
      getHeightMinutes: function(height) {
 -        return Math.round(height * this.timeGranularity / this.granularityUnitHeights);
 +        var d = (24 * 60) / this.mainBody.getHeight();
 +        return Math.round(d * height);
      },
 -    
 +
 +
      /**
       * fetches elements from our generated dom
       */
      initElements : function(){
          var E = Ext.Element;
  
 -//        var el = this.el.dom.firstChild;
          var cs = this.el.dom.firstChild.childNodes;
  
 -//        this.el = new E(el);
 -
          this.mainWrap = new E(cs[0]);
          this.mainHd = new E(this.mainWrap.dom.firstChild);
  
          this.innerHd = this.mainHd.dom.firstChild;
          
 -        this.wholeDayScroller = this.innerHd.firstChild.childNodes[1];
 -        this.wholeDayArea = this.wholeDayScroller.firstChild;
 +        this.wholeDayScroller = new E(this.innerHd.firstChild.childNodes[1]);
 +        this.wholeDayArea = this.wholeDayScroller.dom.firstChild;
          
          this.scroller = new E(this.mainWrap.dom.childNodes[1]);
          this.scroller.setStyle('overflow-x', 'hidden');
          this.mon(this.scroller, 'scroll', this.onBeforeScroll, this);
          this.mon(this.scroller, 'scroll', this.onScroll, this, {buffer: 200});
  
 -        this.mainBody = new E(this.scroller.dom.firstChild);
 -        this.dayCols = this.mainBody.dom.firstChild.lastChild.childNodes;
 +
 +        this.cropper = new E(this.scroller.dom.firstChild);
 +        this.mainBody = new E(this.cropper.dom.firstChild);
 +        this.dayCols = this.mainBody.dom.lastChild.childNodes;
  
          this.focusEl = new E(this.el.dom.lastChild.lastChild);
          this.focusEl.swallowEvent("click", true);
          
          this.el.setSize(csize.width, csize.height);
          
 -        // layout whole day area
 -        var wholeDayAreaEl = Ext.get(this.wholeDayArea);
 -        for (var i=0, bottom = wholeDayAreaEl.getTop(); i<this.wholeDayArea.childNodes.length -1; i++) {
 -            bottom = Math.max(parseInt(Ext.get(this.wholeDayArea.childNodes[i]).getBottom(), 10), bottom);
 -        }
 -        var wholeDayAreaHeight = bottom - wholeDayAreaEl.getTop() + 10;
 -        // take one third of the available height maximum
 -        wholeDayAreaEl.setHeight(wholeDayAreaHeight);
 -        Ext.fly(this.wholeDayScroller).setHeight(Math.min(Math.round(csize.height/3), wholeDayAreaHeight));
 +        // layout whole day area -> take one third of the available height maximum
 +        var wholeDayAreaEl = Ext.get(this.wholeDayArea),
 +            wholeDayAreaHeight = this.computeAllDayAreaHeight(),
 +            wholeDayScrollerHeight = wholeDayAreaHeight,
 +            maxAllowedHeight = Math.round(csize.height/3),
 +            resizeEvent = {
 +                wholeDayAreaHeight: wholeDayAreaHeight,
 +                wholeDayScrollerHeight: wholeDayScrollerHeight,
 +                maxAllowedHeight: maxAllowedHeight
 +            };
 +
 +        wholeDayAreaEl.setHeight(Math.min(wholeDayAreaHeight, maxAllowedHeight));
 +        this.fireEvent('onBeforeAllDayScrollerResize', this, resizeEvent);
 +
 +        this.wholeDayScroller.setHeight(resizeEvent.wholeDayScrollerHeight);
          
          var hdHeight = this.mainHd.getHeight();
          var vh = csize.height - (hdHeight);
          
          this.scroller.setSize(vw, vh);
 -        
 +
 +        // resize mainBody for visibleMinutes to fit
 +        var timeToDisplay = this.timeVisible.getTime() / Date.msMINUTE,
 +            scrollerHeight = this.scroller.getHeight(),
 +            height = scrollerHeight * (24 * 60)/timeToDisplay;
 +
 +        this.mainBody.setHeight(height);
 +
 +        if (this.cropDayTime) {
 +            var cropHeightPx = this.getTimeHeight(this.dayStart, this.dayEnd),
 +                cropStartPx = this.getTimeOffset(this.dayStart);
 +
 +            this.cropper.setStyle('height', cropHeightPx + 'px');
 +            this.cropper.dom.scrollTop = cropStartPx;
 +        }
 +
          // force positioning on scroll hints
 +        this.onBeforeScroll.defer(50, this);
          this.onScroll.defer(100, this);
      },
 -    
 +
 +    computeAllDayAreaHeight: function() {
 +        var wholeDayAreaEl = Ext.get(this.wholeDayArea);
 +        for (var i=0, bottom = wholeDayAreaEl.getTop(); i<this.wholeDayArea.childNodes.length -1; i++) {
 +            bottom = Math.max(parseInt(Ext.get(this.wholeDayArea.childNodes[i]).getBottom(), 10), bottom);
 +        }
 +
 +        // take one third of the available height maximum
 +        return bottom - wholeDayAreaEl.getTop() + this.minAllDayScrollerHight;
 +    },
 +
      onDestroy: function() {
          this.removeAllEvents();
          this.initData(false);
              var day = this.startDate.add(Date.DAY, i);
              html += this.templates.dayHeader.applyTemplate({
                  day: String.format(this.dayFormatString, day.format('l'), day.format('j'), day.format('F')),
 -                height: this.granularityUnitHeights,
 +                height: '20px',
                  width: width + '%',
                  left: i * width + '%'
              });
          for (var i=0; i<this.numOfDays; i++) {
              html += this.templates.wholeDayCol.applyTemplate({
                  //day: date.get('dateString'),
 -                //height: this.granularityUnitHeights,
                  id: baseId + ':' + i,
                  width: width + '%',
                  left: i * width + '%'
  
              html += this.templates.timeRow.applyTemplate({
                  cls: time.get('minutes')%60 ? 'cal-daysviewpanel-timeRow-off' : 'cal-daysviewpanel-timeRow-on',
 -                height: this.granularityUnitHeights + 'px',
 -                top: index * this.granularityUnitHeights + 'px',
 +                height: 100/(24 * 60 / this.timeGranularity) + '%',
                  time: time.get('minutes')%60 ? '' : time.get('time')
              });
          }, this);
       * gets HTML fragment of the time over rows
       */
      getOverRows: function(dayIndex) {
 -        var html = '';
 -        var baseId = Ext.id();
 +        var html = '',
 +            baseId = Ext.id();
          
 -        this.timeScale.each(function(time){
 -            var index = time.get('index');
 +        this.timeScale.each(function(time, index, totalCount) {
 +            var cls = 'cal-daysviewpanel-daycolumn-row-' + (time.get('minutes')%60 ? 'off' : 'on');
 +            if (index+1 == totalCount) {
 +                cls += ' cal-daysviewpanel-daycolumn-row-last';
 +            }
 +
              html += this.templates.overRow.applyTemplate({
                  id: baseId + ':' + dayIndex + ':' + index,
 -                cls: 'cal-daysviewpanel-daycolumn-row-' + (time.get('minutes')%60 ? 'off' : 'on'),
 -                height: this.granularityUnitHeights + 'px',
 +                cls: cls,
 +                height: 100/(24 * 60 / this.timeGranularity) + '%',
                  time: time.get('time')
              });
          }, this);
          ts.master = new Ext.XTemplate(
              '<div class="cal-daysviewpanel" hidefocus="true">',
                  '<div class="cal-daysviewpanel-viewport">',
 -                    '<div class="cal-daysviewpanel-header"><div class="cal-daysviewpanel-header-inner"><div class="cal-daysviewpanel-header-offset">{header}</div></div><div class="x-clear"></div></div>',
 -                    '<div class="cal-daysviewpanel-scroller"><div class="cal-daysviewpanel-body">{body}</div></div>',
 +                    '<div class="cal-daysviewpanel-header">',
 +                        '<div class="cal-daysviewpanel-header-inner">',
 +                            '<div class="cal-daysviewpanel-header-offset">{header}</div>',
 +                        '</div>',
 +                    '<div class="x-clear"></div>',
                  '</div>',
 -                '<a href="#" class="cal-daysviewpanel-focus" tabIndex="-1"></a>',
 -            '</div>'
 +                '<div class="cal-daysviewpanel-scroller">',
 +                    '<div class="cal-daysviewpanel-cropper">{body}</div>',
 +                '</div>',
 +            '</div>',
 +            '<a href="#" class="cal-daysviewpanel-focus" tabIndex="-1"></a>'
          );
          
          ts.header = new Ext.XTemplate(
          );
          
          ts.body = new Ext.XTemplate(
 -            '<div class="cal-daysviewpanel-body-inner">' +
 -                '{timeRows}' +
 -                '<div class="cal-daysviewpanel-body-daycolumns">{dayColumns}</div>' +
 +            '<div class="cal-daysviewpanel-body">',
 +                '<div class="cal-daysviewpanel-body-timecolumn">{timeRows}</div>',
 +                '<div class="cal-daysviewpanel-body-daycolumns">{dayColumns}</div>',
              '</div>'
          );
          
          );
          
          ts.dayColumn = new Ext.XTemplate(
 -            '<div class="cal-daysviewpanel-body-daycolumn" style="left: {left}; width: {width};">',
 +            '<div class="cal-daysviewpanel-body-daycolumn" style="left: {left}; width: {width}; height: 100%">',
                  '<div class="cal-daysviewpanel-body-daycolumn-inner">&#160;</div>',
                  '{overRows}',
                  '<img src="', Ext.BLANK_IMAGE_URL, '" class="cal-daysviewpanel-body-daycolumn-hint-above" />',
@@@ -204,7 -204,37 +204,7 @@@ Tine.Calendar.MainScreenCenterPanel = E
                  }]
              }
          });
 -
 -        /**
 -         * @type {Ext.Action}
 -         */
 -        this.actions_exportEvents = new Ext.Action({
 -            requiredGrant: 'exportGrant',
 -            text: this.app.i18n._('Export Events'),
 -            translationObject: this.app.i18n,
 -            iconCls: 'action_export',
 -            scope: this,
 -            allowMultiple: true,
 -            menu: {
 -                items: [
 -                    new Tine.Calendar.ExportButton({
 -                        text: this.app.i18n._('Export as ODS'),
 -                        format: 'ods',
 -                        iconCls: 'tinebase-action-export-ods',
 -                        exportFunction: 'Calendar.exportEvents',
 -                        gridPanel: this
 -                    }),
 -                    new Tine.Calendar.ExportButton({
 -                        text: this.app.i18n._('Export as ...'),
 -                        iconCls: 'tinebase-action-export-xls',
 -                        exportFunction: 'Calendar.exportEvents',
 -                        showExportDialog: true,
 -                        gridPanel: this
 -                    })
 -                ]
 -            }
 -        });
 -
 +        
          this.showSheetView = new Ext.Button({
              pressed: this.isActiveView('Sheet'),
              scale: 'medium',
              enableToggle: true,
              toggleGroup: 'Calendar_Toolbar_tgViews'
          });
 +        this.toggleFullScreen = new Ext.Toolbar.Button({
 +            text: '\u2197',
 +            scope: this,
 +            handler: function() {
 +                if (this.ownerCt.ref == 'tineViewportMaincardpanel') {
 +                    Tine.Tinebase.viewport.tineViewportMaincardpanel.remove(this, false);
 +                    Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.setActiveItem(Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.lastActiveItem);
 +                    this.originalOwner.add(this);
 +                    this.originalOwner.layout.setActiveItem(this);
 +                    this.toggleFullScreen.setText('\u2197');
 +                    this.southPanel.expand();
 +                } else {
 +                    this.originalOwner = this.ownerCt;
 +                    this.originalOwner.remove(this, false);
 +                    Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.lastActiveItem = Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.activeItem;
 +                    Tine.Tinebase.viewport.tineViewportMaincardpanel.add(this);
 +                    Tine.Tinebase.viewport.tineViewportMaincardpanel.layout.setActiveItem(this);
 +                    this.toggleFullScreen.setText('\u2199');
 +                    this.southPanel.collapse();
 +                }
 +            }
 +        });
          
         this.action_import = new Ext.Action({
              requiredGrant: 'addGrant',
              text: this.app.i18n._('Import Events'),
              disabled: false,
              handler: this.onImport,
 -            scale: 'medium',
              minWidth: 60,
 -            rowspan: 2,
 -            iconAlign: 'top',
              requiredGrant: 'readGrant',
              iconCls: 'action_import',
              scope: this,
              allowMultiple: true
          });
  
 +        this.action_export = new Tine.Calendar.ExportButton({
 +            text: this.app.i18n._('Export Events'),
 +            iconCls: 'action_export',
 +            exportFunction: 'Calendar.exportEvents',
 +            showExportDialog: true,
 +            gridPanel: this
 +        });
 +
          this.changeViewActions = [
              this.showDayView,
              this.showWeekView,
              this.changeViewActions.push(this.showYearView);
          }
  
 +        this.changeViewActions.push(this.toggleFullScreen);
 +
          this.recordActions = [
              this.action_editInNewWindow,
              this.action_deleteRecord
              rows: 2,
              frame: false,
              items: [
 -                this.action_import
 +                this.action_import,
 +                this.action_export
              ]
          }, {
              xtype: 'buttongroup',
          if (this.detailsPanel) {
              this.items.push({
                  region: 'south',
 +                ref: 'southPanel',
                  border: false,
                  collapsible: true,
                  collapseMode: 'mini',
       * @param {Date} datetime
       */
      onPasteEvent: function(datetime) {
-         var record = Tine.Tinebase.data.Clipboard.pull('Calendar', 'Event');
-         var isCopy = record.isCopy;
+         var record = Tine.Tinebase.data.Clipboard.pull('Calendar', 'Event'),
+             isCopy = record.isCopy,
+             sourceView = record.view,
+             sourceRecord = record,
+             sourceViewAttendee = sourceView.ownerCt.attendee,
+             destinationView = this.getCalendarPanel(this.activeView).getView(),
+             destinationViewAttendee = destinationView.ownerCt.attendee;
  
          if (! record) {
              return;
          }
          
-         var dtend   = record.get('dtend');
-         var dtstart = record.get('dtstart');
-         var eventLength = dtend - dtstart;
+         var dtend   = record.get('dtend'),
+             dtstart = record.get('dtstart'),
+             eventLength = dtend - dtstart,
+             store = this.getStore();
+         record.beginEdit();
  
          if (isCopy != true) {
-             // remove before update
-             var store = this.getStore();
+             // remove from ui before update
              var oldRecord = store.getAt(store.findExact('id', record.getId()));
              if (oldRecord && oldRecord.hasOwnProperty('ui')) {
                  oldRecord.ui.remove();
              }, this);
          }
  
-         // Allow to change attendee if in split view
-         var defaultAttendee = Tine.Calendar.Model.Event.getDefaultData().attendee;
-         if (record.data.attendee[0].user_id.account_id != defaultAttendee[0].user_id.account_id) {
-             record.data.attendee[0] = Tine.Calendar.Model.Event.getDefaultData().attendee[0];
+         // @TODO move to common function with daysView::notifyDrop parts
+         // change attendee in split view
+         if (sourceViewAttendee || destinationViewAttendee) {
+             var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(sourceRecord.get('attendee')),
+                 sourceAttendee = sourceViewAttendee ? Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, sourceViewAttendee) : false,
+                 destinationAttendee = destinationViewAttendee ? Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord(attendeeStore, destinationViewAttendee) : false;
+             if (destinationViewAttendee && !destinationAttendee) {
+                 destinationAttendee = new Tine.Calendar.Model.Attender(destinationViewAttendee.data);
+                 attendeeStore.remove(sourceAttendee);
+                 attendeeStore.add(destinationAttendee);
+                 Tine.Calendar.Model.Attender.getAttendeeStore.getData(attendeeStore, record);
+             }
+         }
+         if (datetime.is_all_day_event) {
+             record.set('dtstart', datetime);
+             record.set('dtend', datetime.clone().add(Date.DAY, 1).add(Date.SECOND, -1));
+             record.set('is_all_day_event', true);
+         } else if (datetime.date_only) {
+             var adoptedDtStart = datetime.clone();
+             adoptedDtStart.setHours(dtstart.getHours());
+             adoptedDtStart.setMinutes(dtstart.getMinutes());
+             adoptedDtStart.setSeconds(dtstart.getSeconds());
+             record.set('dtstart', adoptedDtStart);
+             record.set('dtend', new Date(adoptedDtStart.getTime() + eventLength));
+         } else {
+             record.set('dtstart', datetime);
+             record.set('dtend', new Date(datetime.getTime() + eventLength));
          }
  
-         record.set('dtstart', datetime);
-         record.set('dtend', new Date(datetime.getTime() + eventLength));
+         record.endEdit();
  
          if (isCopy == true) {
              record.isCopy = true;
              
              // generate html for each busy attender
              var busyAttendeeHTML = '';
 +            var denyIgnore = false;
 +
              Ext.each(busyAttendee, function(busyAttender) {
                  // TODO refactore name handling of attendee
                  //      -> attender model needs knowlege of how to get names!
                      if (fbInfo.event && fbInfo.event.summary) {
                          eventInfo += ' : ' + fbInfo.event.summary;
                      }
 +                    if (fbInfo.type == 'BUSY_UNAVAILABLE') {
 +                        denyIgnore = true;
 +                        eventInfo += '<span class="cal-conflict-eventinfos-unavailable">' + this.app.i18n._('Unavailable') + '</span>';
 +                    }
                      eventInfos.push(eventInfo);
                  }, this);
                  busyAttendeeHTML += '<div class="cal-conflict-eventinfos">' + eventInfos.join(', <br />') + '</div>';
                  
 -            });
 +            }, this);
              
              this.conflictConfirmWin = Tine.widgets.dialog.MultiOptionsDialog.openWindow({
                  modal: true,
                                 '</div>' +
                                 busyAttendeeHTML,
                  options: [
 -                    {text: this.app.i18n._('Ignore Conflict'), name: 'ignore'},
 +                    {text: this.app.i18n._('Ignore Conflict'), name: 'ignore', disabled: denyIgnore},
                      {text: this.app.i18n._('Edit Event'), name: 'edit', checked: true},
                      {text: this.app.i18n._('Cancel this action'), name: 'cancel'}
                  ],
@@@ -166,10 -166,13 +166,12 @@@ class Crm_Controller_Lead extends Tineb
          
          $view->updater = $_updater;
          $view->lead = $_lead;
 -        $settings = Crm_Controller::getInstance()->getConfigSettings();
 -        $view->leadState = $settings->getOptionById($_lead->leadstate_id, 'leadstates');
 -        $view->leadType = $settings->getOptionById($_lead->leadtype_id, 'leadtypes');
 -        $view->leadSource = $settings->getOptionById($_lead->leadsource_id, 'leadsources');
 +        $view->leadState = Crm_Config::getInstance()->get(Crm_Config::LEAD_STATES)->getTranslatedValue($_lead->leadstate_id);
 +        $view->leadType = Crm_Config::getInstance()->get(Crm_Config::LEAD_TYPES)->getTranslatedValue($_lead->leadtype_id);
 +        $view->leadSource = Crm_Config::getInstance()->get(Crm_Config::LEAD_SOURCES)->getTranslatedValue($_lead->leadsource_id);
          $view->container = Tinebase_Container::getInstance()->getContainerById($_lead->container_id);
+         $view->tags = Tinebase_Tags::getInstance()->getTagsOfRecord($_lead);
+         $view->updates = $this->_getNotificationUpdates($_lead, $_oldLead);
          
          if (isset($_lead->relations)) {
              $customer = $_lead->relations->filter('type', 'CUSTOMER')->getFirstRecord();
          $view->lang_folder = $translate->_('Folder');
          $view->lang_updatedBy = $translate->_('Updated by');
          $view->lang_updatedFields = $translate->_('Updated Fields:');
-         $view->lang_updatedFieldMsg = $translate->_('%s changed from %s to %s.');
+         $view->lang_updatedFieldMsg = $translate->_("'%s' changed from '%s' to '%s'.");
+         $view->lang_tags = $translate->_('Tags');
          
          $plain = $view->render('newLeadPlain.php');
          $html = $view->render('newLeadHtml.php');
          
          return $recipients;
      }
+     /**
+      * get udpate diff for notification
+      *
+      * @param $lead
+      * @param $oldLead
+      * @return array
+      *
+      * TODO generalize
+      * TODO translate field names (modelconfig?)
+      */
+     protected function _getNotificationUpdates($lead, $oldLead)
+     {
+         if (! $oldLead) {
+             return array();
+         }
+         $result = array();
+         foreach ($lead->diff($oldLead, array('seq', 'notes', 'tags', 'relations', 'last_modified_time', 'last_modified_by'))->diff
+              as $key => $value)
+         {
+             $result[] = array(
+                 'modified_attribute' => $key,
+                 'old_value' => $value,
+                 'new_value' => $lead->{$key}
+             );
+         }
+         return $result;
+     }
      
      /**
       * inspect creation of one record
@@@ -493,7 -493,7 +493,7 @@@ class Setup_Controlle
          $setupXml = $this->getSetupXml($_application->name);
          $messages = array();
          
 -        switch(version_compare($_application->version, $setupXml->version)) {
 +        switch (version_compare($_application->version, $setupXml->version)) {
              case -1:
                  $message = "Executing updates for " . $_application->name . " (starting at " . $_application->version . ")";
                  
                  
                  $className = ucfirst($_application->name) . '_Setup_Update_Release' . $_majorVersion;
                  if(! class_exists($className)) {
 -                    Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
 -                        . " update class {$className} does not exists, skipping release {$_majorVersion} for app {$_application->name}"
 +                    $nextMajorRelease = ($_majorVersion + 1) . ".0";
 +                    Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
 +                        . " Update class {$className} does not exists, skipping release {$_majorVersion} for app "
 +                        . "{$_application->name} and increasing version to $nextMajorRelease"
                      );
 +                    $_application->version = $nextMajorRelease;
 +                    Tinebase_Application::getInstance()->updateApplication($_application);
 +
                  } else {
                      $update = new $className($this->_backend);
                  
          Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Installing applications: ' . print_r(array_keys($applications), true));
          
          foreach ($applications as $name => $xml) {
-             $this->_installApplication($xml, $_options);
+             if (! $xml) {
+                 Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Could not install application ' . $name);
+             } else {
+                 $this->_installApplication($xml, $_options);
+             }
          }
      }
  
          }
          
          try {
 +            if (Setup_Core::isLogLevel(Zend_Log::INFO)) Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Installing application: ' . $_xml->name);
 +
              $createdTables = array();
              if (isset($_xml->tables)) {
                  foreach ($_xml->tables[0] as $tableXML) {
                      $table = Setup_Backend_Schema_Table_Factory::factory('Xml', $tableXML);
                      $currentTable = $table->name;
 -                    
 +
 +                    if (Setup_Core::isLogLevel(Zend_Log::DEBUG)) Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Creating table: ' . $currentTable);
 +
                      try {
                          $this->_backend->createTable($table);
                      } catch (Zend_Db_Statement_Exception $zdse) {
                  'order'     => $_xml->order ? (string)$_xml->order : 99,
                  'version'   => (string)$_xml->version
              ));
 -            
 -            Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' installing application: ' . $_xml->name);
 -            
 +
              $application = Tinebase_Application::getInstance()->addApplication($application);
              
              // keep track of tables belonging to this application
              
              Setup_Initialize::initialize($application, $_options);
          } catch (Exception $e) {
 -            $table = (isset($currentTable)) ? ' Table: ' . $currentTable : '';
 -            Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' error at installing: ' . $_xml->name . $table . ' Exception: ' . $e->getMessage() . ' Trace: ' . $e->getTraceAsString());
 +            Tinebase_Exception::log($e, /* suppress trace */ false);
              throw $e;
          }
      }
@@@ -840,14 -840,10 +840,14 @@@ class Tinebase_Container extends Tineba
              )
              
              ->where("{$this->_db->quoteIdentifier('container.application_id')} = ?", $application->getId())
 -            ->where("{$this->_db->quoteIdentifier('container.type')} = ?", Tinebase_Model_Container::TYPE_SHARED)
 -            
 -            ->order('container.name');
 -        
 +            ->where("{$this->_db->quoteIdentifier('container.type')} = ?", Tinebase_Model_Container::TYPE_SHARED);
 +
 +        // TODO maybe this could be removed or changed to a preference later
 +        $sortOrder = Tinebase_Config::getInstance()->featureEnabled(Tinebase_Config::FEATURE_CONTAINER_CUSTOM_SORT)
 +            ? array('container.order', 'container.name')
 +            : 'container.name';
 +        $select->order($sortOrder);
 +
          $this->addGrantsSql($select, $accountId, $grant, 'container_acl', $_andGrants, __CLASS__ . '::addGrantsSqlCallback');
          
          $stmt = $this->_db->query('/*' . __FUNCTION__ . '*/' . $select);
          $classCacheId = $accountId . $containerId . $container->seq . $_grantModel;
          
          try {
-             return $this->loadFromClassCache(__FUNCTION__, $classCacheId, Tinebase_Cache_PerRequest::VISIBILITY_SHARED);
+             $grants = $this->loadFromClassCache(__FUNCTION__, $classCacheId, Tinebase_Cache_PerRequest::VISIBILITY_SHARED);
+             if ($grants instanceof Tinebase_Model_Grants) {
+                 return $grants;
+             } else {
+                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
+                     ' Invalid data in cache ... fetching fresh data from DB');
+             }
          } catch (Tinebase_Exception_NotFound $tenf) {
-             
+             // not found in cache
          }
          
          $select = $this->_getAclSelectByContainerId($containerId)