Merge branch '2014.09' into 2015.07
authorPhilipp Schüle <p.schuele@metaways.de>
Thu, 16 Jul 2015 10:01:21 +0000 (12:01 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Thu, 16 Jul 2015 10:01:21 +0000 (12:01 +0200)
85 files changed:
scripts/packaging/build-tine20-packages.sh
tests/tine20/Calendar/Controller/EventTests.php
tests/tine20/Calendar/Controller/RecurTest.php
tests/tine20/Calendar/Convert/Event/VCalendar/GenericTest.php
tests/tine20/Felamimail/Controller/MessageTest.php
tests/tine20/Zend/Db/SelectTest.php
tine20/ActiveSync/Setup/Update/Release8.php
tine20/ActiveSync/Setup/setup.xml
tine20/Addressbook/Convert/Contact/VCard/CardDAVSync.php [new file with mode: 0755]
tine20/Addressbook/Convert/Contact/VCard/Factory.php
tine20/Addressbook/Setup/Update/Release8.php
tine20/Addressbook/Setup/setup.xml
tine20/Admin/Setup/Update/Release8.php
tine20/Admin/Setup/setup.xml
tine20/Calendar/Backend/Sql.php
tine20/Calendar/Calendar.jsb2
tine20/Calendar/Config.php
tine20/Calendar/Controller/Event.php
tine20/Calendar/Convert/Event/VCalendar/Abstract.php
tine20/Calendar/Frontend/Json.php
tine20/Calendar/Setup/Update/Release8.php
tine20/Calendar/Setup/setup.xml
tine20/Calendar/css/Calendar.css
tine20/Calendar/css/print.css
tine20/Calendar/css/yearviewpanel.css [new file with mode: 0644]
tine20/Calendar/js/EventUI.js
tine20/Calendar/js/MainScreenCenterPanel.js
tine20/Calendar/js/PagingToolbar.js
tine20/Calendar/js/Printer/Base.js
tine20/Calendar/js/Printer/YearView.js [new file with mode: 0644]
tine20/Calendar/js/YearView.js [new file with mode: 0644]
tine20/Courses/Setup/Update/Release8.php
tine20/Courses/Setup/setup.xml
tine20/Crm/Setup/Update/Release8.php
tine20/Crm/Setup/setup.xml
tine20/Crm/js/LeadGridDetailsPanel.js
tine20/Felamimail/Controller/Message.php
tine20/Felamimail/Controller/Message/Send.php
tine20/Felamimail/Message.php
tine20/Felamimail/Setup/Update/Release8.php
tine20/Felamimail/Setup/setup.xml
tine20/Felamimail/js/GridPanelHook.js
tine20/Filemanager/Setup/Update/Release8.php
tine20/Filemanager/Setup/setup.xml
tine20/HumanResources/Setup/Update/Release8.php
tine20/HumanResources/Setup/setup.xml
tine20/Inventory/Setup/Update/Release8.php [new file with mode: 0644]
tine20/Inventory/Setup/setup.xml
tine20/Phone/Setup/Update/Release8.php [new file with mode: 0644]
tine20/Phone/Setup/setup.xml
tine20/Projects/Setup/Update/Release8.php [new file with mode: 0644]
tine20/Projects/Setup/setup.xml
tine20/Sales/Backend/Contract.php
tine20/Sales/Controller/Invoice.php
tine20/Sales/Setup/Update/Release8.php
tine20/Sales/Setup/setup.xml
tine20/Sales/js/ContractEditDialog.js
tine20/Sales/js/CustomerEditDialog.js
tine20/Sales/js/InvoiceEditDialog.js
tine20/Sales/js/PurchaseInvoiceEditDialog.js
tine20/Sales/js/SupplierEditDialog.js
tine20/Setup/Backend/Mysql.php
tine20/Setup/Exception/PromptUser.php
tine20/Setup/Frontend/Json.php
tine20/Setup/Update/Abstract.php
tine20/Tasks/Setup/Update/Release8.php
tine20/Tasks/Setup/setup.xml
tine20/Timetracker/Setup/Update/Release8.php
tine20/Timetracker/Setup/setup.xml
tine20/Timetracker/js/TimeaccountEditDialog.js
tine20/Tinebase/Auth.php
tine20/Tinebase/Controller.php
tine20/Tinebase/Export/Spreadsheet/Ods.php
tine20/Tinebase/Group/Ldap.php
tine20/Tinebase/Helper.php
tine20/Tinebase/Relations.php
tine20/Tinebase/Setup/Update/Release8.php
tine20/Tinebase/Setup/setup.xml
tine20/Tinebase/WebDav/PrincipalBackend.php
tine20/Tinebase/js/ExceptionHandler.js
tine20/Tinebase/js/extInit.js
tine20/Tinebase/js/ux/file/Upload.js
tine20/Voipmanager/Setup/Update/Release8.php
tine20/Voipmanager/Setup/setup.xml
tine20/bootstrap.php

index 55c67bd..457fb39 100755 (executable)
@@ -11,11 +11,11 @@ BASEDIR=`readlink -f ./tine20build`
 TEMPDIR="$BASEDIR/temp"
 MISCPACKAGESDIR="$BASEDIR/packages/misc"
 
-CODENAME="Koriander"
+CODENAME="Elena"
 GITURL="http://git.tine20.org/git/tine20"
 
 RELEASE=""
-GITBRANCH="2014.09"
+GITBRANCH="2015.07"
 PACKAGEDIR=""
 
 PATH=$MISCPACKAGESDIR:$TEMPDIR/tine20/vendor/bin:$PATH
index 941680d..8e31c14 100644 (file)
@@ -1487,4 +1487,19 @@ class Calendar_Controller_EventTests extends Calendar_TestCase
         $ownAttender = Calendar_Model_Attender::getOwnAttender($repairedEvent->attendee);
         $this->assertTrue($ownAttender !== null);
     }
+
+    /**
+     * @see 0011130: handle bad originator timzone in VCALENDAR converter
+     */
+    public function testBrokenTimezoneInEvent()
+    {
+        $event = $this->_getEvent(true);
+        $event->originator_tz = 'AWST';
+        try {
+            $event = $this->_controller->create($event);
+            $this->fail('should throw Tinebase_Exception_Record_Validation because of bad TZ: ' . print_r($event->toArray(), true));
+        } catch (Tinebase_Exception_Record_Validation $terv) {
+            $this->assertEquals('Bad Timezone: AWST', $terv->getMessage());
+        }
+    }
 }
index a58ab5b..0c2c86c 100644 (file)
@@ -677,15 +677,49 @@ class Calendar_Controller_RecurTest extends Calendar_TestCase
         
         $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event');
         $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($persistentEvent, $exceptions, $from, $until);
-        
-        $recurSet[5]->attendee->addRecord(new Calendar_Model_Attender(array(
+
+        $pwulf = new Calendar_Model_Attender(array(
             'user_type'   => Calendar_Model_Attender::USERTYPE_USER,
             'user_id'     => $this->_getPersonasContacts('pwulf')->getId()
-        )));
+        ));
+        $recurSet[5]->attendee->addRecord($pwulf);
         
         $updatedPersistentEvent = $this->_controller->createRecurException($recurSet[5], FALSE, TRUE);
         
         $this->assertEquals(3, count($updatedPersistentEvent->attendee));
+
+        $persistentPwulf = Calendar_Model_Attender::getAttendee($updatedPersistentEvent->attendee, $pwulf);
+        $this->assertNotNull($persistentPwulf->displaycontainer_id);
+    }
+    
+    /**
+     * Events don't show up in attendees personal calendar
+     */
+    public function testCreateRecurExceptionAllFollowingAttendeeAdd2()
+    {
+        $from = new Tinebase_DateTime('2014-04-01 00:00:00');
+        $until = new Tinebase_DateTime('2014-04-29 23:59:59');
+        
+        $persistentEvent = $this->_getDailyEvent(new Tinebase_DateTime('2014-04-03 09:00:00'));
+        
+        $exceptions = new Tinebase_Record_RecordSet('Calendar_Model_Event');
+        $recurSet = Calendar_Model_Rrule::computeRecurrenceSet($persistentEvent, $exceptions, $from, $until);
+        
+        $recurSet[5]->attendee->addRecord(new Calendar_Model_Attender(array(
+                'user_type'   => Calendar_Model_Attender::USERTYPE_USER,
+                'user_id'     => $this->_getPersonasContacts('pwulf')->getId()
+        )));
+        
+        $updatedPersistentEvent = $this->_controller->createRecurException($recurSet[5], FALSE, TRUE);
+        $this->assertEquals(3, count($updatedPersistentEvent->attendee));
+        
+        $filter = new Calendar_Model_EventFilter(array(
+                array('field' => 'container_id',             'operator' => 'equals', 'value' => $this->_personasDefaultCals['pwulf']->id),
+                array('field' => 'attender_status', 'operator' => 'not',    'value' => Calendar_Model_Attender::STATUS_DECLINED),
+        ));
+        
+        $events = $this->_controller->search($filter);
+        $this->assertEquals(1, count($events), 'event should be found, but is not');
     }
     
     /**
index 5f2ccdc..78dd6c3 100644 (file)
@@ -758,4 +758,22 @@ class Calendar_Convert_Event_VCalendar_GenericTest extends PHPUnit_Framework_Tes
         
         $this->assertEquals('Telko axel', $savedEvent->summary);
     }
+
+    /**
+     * @see 0011130: handle bad originator timzone in VCALENDAR converter
+     */
+    public function testBrokenTimezoneInTineEvent()
+    {
+        $event = $this->testConvertRepeatingAllDayDailyEventToTine20Model();
+        $event->originator_tz = 'AWST'; // Australian Western Standard Time
+
+        $this->_converter = Calendar_Convert_Event_VCalendar_Factory::factory(Calendar_Convert_Event_VCalendar_Factory::CLIENT_GENERIC);
+
+        try {
+            $vevent = $this->_converter->fromTine20Model($event)->serialize();
+            $this->fail('should throw Tinebase_Exception_Record_Validation because of bad TZ');
+        } catch (Tinebase_Exception_Record_Validation $terv) {
+            $this->assertEquals('Bad Timezone: AWST', $terv->getMessage());
+        }
+    }
 }
index 99dad18..2f65e9d 100644 (file)
@@ -1686,7 +1686,7 @@ Photographer', $message->body);
     {
         $cachedMessage = $this->messageTestHelper('html_jump_labels.eml');
         $message = $this->_controller->getCompleteMessage($cachedMessage);
-        $this->assertContains('<a href="#felamimail_inline_test" target="_self">test</a>
+        $this->assertContains('<a href="#felamimail_inline_test" target="_blank">test</a>
 <p>Hello,</p>
 <p id="felamimail_inline_test">Text Content</p>', $message->body);
     }
index eb523b5..31d0632 100644 (file)
@@ -31,7 +31,7 @@ class Zend_Db_SelectTest extends TestCase
             $this->assertEquals(' ORDER BY `id; SLEEP(1)` ASC', $select->__toString());
         } else if (Tinebase_Core::getDb() instanceof Zend_Db_Adapter_Pdo_Pgsql) {
             $select->order('id; SELECT PG_SLEEP(5)');
-            $this->assertEquals(' ORDER BY `id; SELECT PG_SLEEP(5)` ASC', $select->__toString());
+            $this->assertEquals(' ORDER BY "id; SELECT PG_SLEEP(5)" ASC', $select->__toString());
         } else {
             $this->markTestSkipped('no test for this adapter yet');
         }
index e82f939..7841222 100644 (file)
@@ -48,4 +48,14 @@ class ActiveSync_Setup_Update_Release8 extends Setup_Update_Abstract
 
         $this->setApplicationVersion('ActiveSync', '8.2');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_2()
+    {
+        $this->setApplicationVersion('ActiveSync', '9.0');
+    }
 }
index 38d05f5..8e51725 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>ActiveSync</name>
-    <version>8.2</version>
+    <version>9.0</version>
     <order>90</order>
     <depends>
         <application>Tinebase</application>
diff --git a/tine20/Addressbook/Convert/Contact/VCard/CardDAVSync.php b/tine20/Addressbook/Convert/Contact/VCard/CardDAVSync.php
new file mode 100755 (executable)
index 0000000..98ab633
--- /dev/null
@@ -0,0 +1,213 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Addressbook
+ * @subpackage  Convert
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Lars Kneschke <l.kneschke@metaways.de>
+ * @copyright   Copyright (c) 2011-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ *
+ */
+
+/**
+2015/05/12 by Ingo Ratsdorf ingo@envirology.co.nz
+CardDAVSync send the following data ONLY:
+
+BEGIN:VCARD
+VERSION:3.0
+FN:Ratsdorf\, Janus
+N:Ratsdorf;Janus;;;
+UID:56e09493b665c30dfbc3de103b20e424d1c709a6
+ORG:;
+TEL;TYPE=WORK:
+TEL;TYPE=HOME:+64 9 411 8 444
+TEL;TYPE=HOME;TYPE=CELL:+64 27 345 1070
+TEL;TYPE=FAX:
+TEL;TYPE=FAX;TYPE=HOME:
+TEL;TYPE=PAGER:
+TEL;TYPE=OTHER:
+ADR;TYPE=WORK:;;;;;;
+ADR;TYPE=HOME:;;35 Taylor Road;Auckland;Waimauku;0882;NEW ZEALAND
+EMAIL;TYPE=WORK:
+EMAIL;TYPE=HOME:janus@envirology.co.nz
+URL;TYPE=WORK:https://www.facebook.com/janus.ratsdorf
+URL;TYPE=HOME:
+NOTE:
+BDAY:2001-09-02
+PHOTO;ENCODING=B;TYPE=JPEG: [..]
+TEL:0000000985
+itemtel333336888.TEL;TYPE=VOICE:333336888
+itemtel333336888.X-ABLABEL:assistant
+TEL;TYPE=PAGER:114114469
+TEL:
+PRODID:-//dmfs.org//mimedir.vcard//EN
+REV:20150512T080704Z
+END:VCARD
+
+*/
+
+
+/**
+ * class to convert a SOGO vcard to contact model and back again
+ *
+ * @package     Addressbook
+ * @subpackage  Convert
+ */
+class Addressbook_Convert_Contact_VCard_CardDAVSync extends Addressbook_Convert_Contact_VCard_Abstract
+{
+    // CardDAV-Sync free/0.4.12 (samsung; kltedv; Android 4.4.2; en_NZ; org.dmfs.carddav.sync/99)
+    const HEADER_MATCH = '/(CardDAV-Sync|CardDAV-Sync free)\/(?P<version>.*)/';
+    
+    protected $_emptyArray = array(
+        'adr_one_countryname'   => null,
+        'adr_one_locality'      => null,
+        'adr_one_postalcode'    => null,
+        'adr_one_region'        => null,
+        'adr_one_street'        => null,
+        'adr_one_street2'       => null,
+        'adr_two_countryname'   => null,
+        'adr_two_locality'      => null,
+        'adr_two_postalcode'    => null,
+        'adr_two_region'        => null,
+        'adr_two_street'        => null,
+        'adr_two_street2'       => null,
+        #'assistent'             => null,
+        'bday'                  => null,
+        #'calendar_uri'          => null,
+        'email'                 => null,
+        'email_home'            => null,
+        'jpegphoto'             => null,
+        #'freebusy_uri'          => null,
+        'note'                  => null,
+        #'role'                  => null,
+        #'salutation'            => null,
+        'title'                 => null,
+        'url'                   => null,
+        'url_home'              => null,
+        'n_family'              => null,
+        'n_fileas'              => null,
+        #'n_fn'                  => null,
+        'n_given'               => null,
+        #'n_middle'              => null,
+        #'n_prefix'              => null,
+        #'n_suffix'              => null,
+        'org_name'              => null,
+        'org_unit'              => null,
+        #'pubkey'                => null,
+        #'room'                  => null,
+        'tel_assistent'         => null,
+        #'tel_car'               => null,
+        'tel_cell'              => null,
+        'tel_cell_private'      => null,
+        'tel_fax'               => null,
+        'tel_fax_home'          => null,
+        'tel_home'              => null,
+        'tel_pager'             => null,
+        'tel_work'              => null,
+        'tel_other'             => null,
+        #'tel_prefer'            => null,
+        #'tz'                    => null,
+        #'geo'                   => null,
+        #'lon'                   => null,
+        #'lat'                   => null,
+        'tags'                  => null,
+        'notes'                 => null,
+    );
+    
+    /**
+     * (non-PHPdoc)
+     * @see Addressbook_Convert_Contact_VCard_Abstract::toTine20Model()
+     */
+    public function toTine20Model($_blob, Tinebase_Record_Abstract $_record = null, $options = array())
+    {
+        $contact = parent::toTine20Model($_blob, $_record, $options);
+       Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' contact (RAW) ' . print_r($_blob, true));
+       Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' tine20 contact ' . print_r($contact->toArray(), true));
+
+        
+        if (!empty($contact->url)) {
+            $contact->url = strtr($contact->url, array('http\:' => 'http:'));
+        }
+        if (!empty($contact->url_home)) {
+            $contact->url_home = strtr($contact->url_home, array('http\:' => 'http:'));
+        }
+        
+        return $contact;
+    }
+    
+    /**
+     * (non-PHPdoc)
+     * @see Addressbook_Convert_Contact_VCard_Abstract::_toTine20ModelParseTel()
+     */
+    protected function _toTine20ModelParseTel(&$data, \Sabre\VObject\Property $property)
+    {
+        if (!isset($property['TYPE'])) {
+            // CardDAVSync sends OTHER just as TEL:12345678 without any TYPE
+            $data['tel_other'] = $property->getValue();
+        }
+
+        parent::_toTine20ModelParseTel($data, $property);
+
+    }
+
+    /**
+     * converts Addressbook_Model_Contact to vcard
+     * 
+     * @param  Addressbook_Model_Contact  $_record
+     * @return string
+     */
+    public function fromTine20Model(Tinebase_Record_Abstract $_record)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' contact ' . print_r($_record->toArray(), true));
+        
+        // initialize vcard object
+        $card = $this->_fromTine20ModelRequiredFields($_record);
+
+        $card->add('TEL', $_record->tel_work, array('TYPE' => 'WORK'));
+
+        $card->add('TEL', $_record->tel_home, array('TYPE' => 'HOME'));
+
+        $card->add('TEL', $_record->tel_cell, array('TYPE' => 'CELL'));
+
+        $card->add('TEL', $_record->tel_cell_private, array('TYPE' => array('CELL', 'HOME')));
+
+        $card->add('TEL', $_record->tel_fax, array('TYPE' => array('FAX')));
+
+        $card->add('TEL', $_record->tel_fax_home, array('TYPE' => array('FAX', 'HOME')));
+
+        $card->add('TEL', $_record->tel_pager, array('TYPE' => 'PAGER'));
+
+        $card->add('TEL', $_record->tel_other, array('TYPE' => 'OTHER'));
+        
+        $card->add('ADR', array(null, $_record->adr_one_street2, $_record->adr_one_street, $_record->adr_one_locality, $_record->adr_one_region, $_record->adr_one_postalcode, $_record->adr_one_countryname), array('TYPE' => 'WORK'));
+        
+        $card->add('ADR', array(null, $_record->adr_two_street2, $_record->adr_two_street, $_record->adr_two_locality, $_record->adr_two_region, $_record->adr_two_postalcode, $_record->adr_two_countryname), array('TYPE' => 'HOME'));
+        
+        $card->add('EMAIL', $_record->email, array('TYPE' => 'WORK'));
+        
+        $card->add('EMAIL', $_record->email_home, array('TYPE' => 'HOME'));
+        
+        $card->add('URL', $_record->url, array('TYPE' => 'WORK'));
+        
+        $card->add('URL', $_record->url_home, array('TYPE' => 'HOME'));
+                
+        $card->add('NOTE', $_record->note);
+        
+        $this->_fromTine20ModelAddBirthday($_record, $card);
+        
+        $this->_fromTine20ModelAddPhoto($_record, $card);
+        
+        $this->_fromTine20ModelAddGeoData($_record, $card);
+        
+        //$this->_fromTine20ModelAddCategories($_record, $card);
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' card ' . $card->serialize());
+        
+        return $card;
+    }
+    
+
+}
index 746fb0e..6b8951a 100644 (file)
@@ -27,6 +27,7 @@ class Addressbook_Convert_Contact_VCard_Factory
     const CLIENT_AKONADI        = 'akonadi';
     const CLIENT_TELEFONBUCH    = 'telefonbuch';
     const CLIENT_DAVDROID       = 'davdroid';
+    const CLIENT_CARDDAVSYNC    = 'org.dmfs.carddav.sync';
     
     /**
      * cache parsed user-agent strings
@@ -88,6 +89,10 @@ class Addressbook_Convert_Contact_VCard_Factory
                 return new Addressbook_Convert_Contact_VCard_DavDroid($_version);
                 break;
 
+            case Addressbook_Convert_Contact_VCard_Factory::CLIENT_CARDDAVSYNC:
+                return new Addressbook_Convert_Contact_VCard_CardDAVSync($_version);
+                   break;
+
             case Addressbook_Convert_Contact_VCard_Factory::CLIENT_TELEFONBUCH:
                 return new Addressbook_Convert_Contact_VCard_Telefonbuch($_version);
         }
@@ -144,6 +149,11 @@ class Addressbook_Convert_Contact_VCard_Factory
             $backend = Addressbook_Convert_Contact_VCard_Factory::CLIENT_DAVDROID;
             $version = $matches['version'];
 
+        // DMFS CardDAVSync
+        } elseif (preg_match(Addressbook_Convert_Contact_VCard_CardDAVSync::HEADER_MATCH, $_userAgent, $matches)) {
+            $backend = Addressbook_Convert_Contact_VCard_Factory::CLIENT_CARDDAVSYNC;
+            $version = $matches['version'];
+        
         // generic client
         } else {
             $backend = Addressbook_Convert_Contact_VCard_Factory::CLIENT_GENERIC;
index df3f6af..afb4ae1 100644 (file)
@@ -41,9 +41,17 @@ class Addressbook_Setup_Update_Release8 extends Setup_Update_Abstract
                 </field>');
             $this->_backend->alterCol('addressbook', $declaration);
         }
-
         $this->setTableVersion('addressbook', 18);
-
         $this->setApplicationVersion('Addressbook', '8.2');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_2()
+    {
+        $this->setApplicationVersion('Addressbook', '9.0');
+    }
 }
index 303f803..79cd9eb 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Addressbook</name>
-    <version>8.2</version>
+    <version>9.0</version>
     <order>10</order>
     <depends>
         <application>Admin</application>
index f1b6a4e..26e0eca 100644 (file)
@@ -31,4 +31,14 @@ class Admin_Setup_Update_Release8 extends Setup_Update_Abstract
         Setup_Controller::getInstance()->createImportExportDefinitions(Tinebase_Application::getInstance()->getApplicationByName('Admin'));
         $this->setApplicationVersion('Admin', '8.2');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_2()
+    {
+        $this->setApplicationVersion('Admin', '9.0');
+    }
 }
index c32d6d0..1ecef69 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Admin</name>
-    <version>8.2</version>
+    <version>9.0</version>
     <order>1</order>
     <depends>
         <application>Tinebase</application>
index ef00355..a9d95dd 100644 (file)
@@ -162,7 +162,7 @@ class Calendar_Backend_Sql extends Tinebase_Backend_Sql_Abstract
             $_pagination = new Tinebase_Model_Pagination();
         }
 
-        $getDeleted = !!$_filter && $_filter->getFilter('is_deleted');
+        $getDeleted = is_object($_filter) && $_filter->getFilter('is_deleted');
         $select = parent::_getSelect('*', $getDeleted);
         
         $select->joinLeft(
index 1c12373..c3d6cca 100644 (file)
           "path": "js/"
         },
         {
+          "text": "YearView.js",
+          "path": "js/"
+        },
+        {
           "text": "GridView.js",
           "path": "js/"
         },
           "path": "js/Printer/"
         },
         {
+          "text": "YearView.js",
+          "path": "js/Printer/"
+        },
+        {
           "text": "ContactEventsGridPanel.js",
           "path": "js/"
         },
           "path": "css/"
         },
         {
+          "text": "yearviewpanel.css",
+          "path": "css/"
+        },
+        {
           "text": "Calendar.css",
           "path": "css/"
         }
index 6b6264c..9a4752b 100644 (file)
@@ -112,6 +112,13 @@ class Calendar_Config extends Tinebase_Config_Abstract
     const FEATURE_SPLIT_VIEW = 'featureSplitView';
 
     /**
+     * FEATURE_YEAR_VIEW
+     *
+     * @var string
+     */
+    const FEATURE_YEAR_VIEW = 'featureYearView';
+
+    /**
      * FEATURE_EXTENDED_EVENT_CONTEXT_ACTIONS
      *
      * @var string
@@ -277,13 +284,18 @@ class Calendar_Config extends Tinebase_Config_Abstract
                     'label'         => 'Calendar Split View', //_('Calendar Split View')
                     'description'   => 'Split day and week views by attendee', //_('Split day and week views by attendee')
                 ),
+                self::FEATURE_YEAR_VIEW => array(
+                    'label'         => 'Calendar Year View', //_('Calendar Year View')
+                    'description'   => 'Adds year view to Calendar', //_('Adds year view to Calendar')
+                ),
                 self::FEATURE_EXTENDED_EVENT_CONTEXT_ACTIONS => array(
                     'label'         => 'Calendar Extended Context Menu Actions', //_('Calendar Extended Context Menu Actions')
                     'description'   => 'Adds extended actions to event context menus', //_('Adds extended actions to event context menus')
                 ),
             ),
             'default'               => array(
-                self::FEATURE_SPLIT_VIEW                        => true,
+                self::FEATURE_SPLIT_VIEW           => true,
+                self::FEATURE_YEAR_VIEW            => true,
                 self::FEATURE_EXTENDED_EVENT_CONTEXT_ACTIONS    => false,
             ),
         ),
index 14c691b..ed41b96 100644 (file)
@@ -1111,17 +1111,10 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
                 $note = $_event->notes; unset($_event->notes);
                 $persistentExceptionEvent = $this->create($_event, $_checkBusyConflicts && $dtStartHasDiff);
                 
-                // we save attendee seperatly to preserve their attributes
+                // we save attendee separately to preserve their attributes
                 if ($attendees instanceof Tinebase_Record_RecordSet) {
-                    $attendees->cal_event_id = $persistentExceptionEvent->getId();
-                    
                     foreach($attendees as $attendee) {
-                        if (! $attendee->status_authkey) {
-                            // new invitations
-                            $attendee->status_authkey = Tinebase_Record_Abstract::generateUID();
-                        }
-                        $this->_backend->createAttendee($attendee);
-                        $this->_increaseDisplayContainerContentSequence($attendee, $persistentExceptionEvent, Tinebase_Model_ContainerContent::ACTION_CREATE);
+                        $this->_createAttender($attendee, $persistentExceptionEvent, true);
                     }
                 }
                 
@@ -1486,10 +1479,11 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
     protected function _inspectEvent($_record)
     {
         $_record->uid = $_record->uid ? $_record->uid : Tinebase_Record_Abstract::generateUID();
-        $_record->originator_tz = $_record->originator_tz ? $_record->originator_tz : Tinebase_Core::getUserTimezone();
         $_record->organizer = $_record->organizer ? $_record->organizer : Tinebase_Core::getUser()->contact_id;
         $_record->transp = $_record->transp ? $_record->transp : Calendar_Model_Event::TRANSP_OPAQUE;
-        
+
+        $this->_inspectOriginatorTZ($_record);
+
         if ($_record->hasExternalOrganizer()) {
             // assert calendarUser as attendee. This is important to keep the event in the loop via its displaycontianer(s)
             try {
@@ -1543,6 +1537,23 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
             $_record->rrule = NULL;
         }
     }
+
+    /**
+     * checks/sets originator timezone
+     *
+     * @param $record
+     * @throws Tinebase_Exception_Record_Validation
+     */
+    protected function _inspectOriginatorTZ($record)
+    {
+        $record->originator_tz = $record->originator_tz ? $record->originator_tz : Tinebase_Core::getUserTimezone();
+
+        try {
+            new DateTimeZone($record->originator_tz);
+        } catch (Exception $e) {
+            throw new Tinebase_Exception_Record_Validation('Bad Timezone: ' . $record->originator_tz);
+        }
+    }
     
     /**
      * inspects delete action
@@ -1989,6 +2000,7 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
     {
         // apply defaults
         $attender->user_type         = isset($attender->user_type) ? $attender->user_type : Calendar_Model_Attender::USERTYPE_USER;
+        $attender->cal_event_id      =  $event->getId();
         $calendar = ($calendar) ? $calendar : Tinebase_Container::getInstance()->getContainerById($event->container_id);
         
         $userAccountId = $attender->getUserAccountId();
index 5e21735..938078e 100644 (file)
@@ -81,8 +81,13 @@ class Calendar_Convert_Event_VCalendar_Abstract extends Tinebase_Convert_VCalend
         if (empty($originatorTz)) {
             throw new Tinebase_Exception_Record_Validation('originator_tz needed for conversion to Sabre\VObject\Component');
         }
-        
-        $vcalendar->add(new Sabre_VObject_Component_VTimezone($originatorTz));
+
+        try {
+            $vcalendar->add(new Sabre_VObject_Component_VTimezone($originatorTz));
+        } catch (Exception $e) {
+            Tinebase_Exception::log($e);
+            throw new Tinebase_Exception_Record_Validation('Bad Timezone: ' . $originatorTz);
+        }
         
         foreach ($_records as $_record) {
             $this->_convertCalendarModelEvent($vcalendar, $_record);
index 26bc770..8413a67 100644 (file)
@@ -229,7 +229,7 @@ class Calendar_Frontend_Json extends Tinebase_Frontend_Json_Abstract
                 'default'               => ($defaultDefinition) ? $definitionConverter->fromTine20Model($defaultDefinition) : array(),
             );
         } catch (Exception $e) {
-            Tinebase_Exception::log($zce);
+            Tinebase_Exception::log($e);
             $result = array(
                 array(
                     'results'               => array(),
index 03ddb92..1fc4ff5 100644 (file)
@@ -248,4 +248,78 @@ class Calendar_Setup_Update_Release8 extends Setup_Update_Abstract
         $this->setTableVersion('cal_events', '9');
         $this->setApplicationVersion('Calendar', '8.7');
     }
+
+    /**
+     * repair missing displaycontainer_id
+     */
+    public function update_7()
+    {
+        $allUser = $this->_db->query(
+            "SELECT " . $this->_db->quoteIdentifier('id') . "," .  $this->_db->quoteIdentifier('contact_id') .
+            " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "accounts") .
+            " WHERE " . $this->_db->quoteIdentifier("contact_id") . " IS NOT NULL"
+        )->fetchAll(Zend_Db::FETCH_ASSOC);
+
+        $contactUserMap = array();
+        foreach ($allUser as $id => $user) {
+            $contactUserMap[$user['contact_id']] = $user['id'];
+        }
+
+        // find all user/groupmember attendees with missing displaycontainer
+        $attendees = $this->_db->query(
+            "SELECT DISTINCT" . $this->_db->quoteIdentifier('user_type') . "," . $this->_db->quoteIdentifier('user_id') .
+            " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
+            " WHERE " . $this->_db->quoteIdentifier("displaycontainer_id") . " IS  NULL" .
+            "  AND " . $this->_db->quoteIdentifier("user_type")  . $this->_db->quoteInto(" IN (?)", array('user', 'groupmemeber')) .
+            "  AND " . $this->_db->quoteIdentifier("user_id")  . $this->_db->quoteInto(" IN (?)", array_keys($contactUserMap))
+        )->fetchAll(Zend_Db::FETCH_ASSOC);
+
+        // find all user/groupmember attendees with missing displaycontainer
+        $attendees = array_merge($attendees, $this->_db->query(
+            "SELECT DISTINCT" . $this->_db->quoteIdentifier('user_type') . "," . $this->_db->quoteIdentifier('user_id') .
+            " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
+            " WHERE " . $this->_db->quoteIdentifier("displaycontainer_id") . " IS  NULL" .
+            "  AND " . $this->_db->quoteIdentifier("user_type")  . $this->_db->quoteInto(" IN (?)", array('resource'))
+        )->fetchAll(Zend_Db::FETCH_ASSOC));
+
+        $resources = $this->_db->query(
+            "SELECT " . $this->_db->quoteIdentifier('id') . "," . $this->_db->quoteIdentifier('container_id') .
+            " FROM " . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_resources")
+        )->fetchAll(Zend_Db::FETCH_ASSOC);
+
+        $resourceContainerMap = array();
+        foreach($resources as $resource) {
+            $resourceContainerMap[$resource['id']] = $resource['container_id'];
+        }
+
+        foreach ($attendees as $attendee) {
+            //find out displaycontainer
+            if ($attendee['user_type'] != 'resource') {
+                $userAccountId = $contactUserMap[$attendee['user_id']];
+                $attendee['displaycontainerId'] = Calendar_Controller_Event::getDefaultDisplayContainerId($userAccountId);
+            } else {
+                $attendee['displaycontainerId'] = $resourceContainerMap[$attendee['user_id']];
+            }
+
+            // update displaycontainer
+            $this->_db->query(
+                "UPDATE" . $this->_db->quoteIdentifier(SQL_TABLE_PREFIX . "cal_attendee") .
+                " SET " . $this->_db->quoteIdentifier("displaycontainer_id") . " = " . $this->_db->quote($attendee['displaycontainerId']) .
+                " WHERE " . $this->_db->quoteIdentifier("user_type") . " = " . $this->_db->quote($attendee['user_type']) .
+                "  AND " . $this->_db->quoteIdentifier("user_id") . " = " . $this->_db->quote($attendee['user_id'])
+            );
+        }
+
+        $this->setApplicationVersion('Calendar', '8.8');
+    }
+
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_8()
+    {
+        $this->setApplicationVersion('Calendar', '9.0');
+    }
 }
index fd77402..ca5b5b1 100644 (file)
@@ -2,7 +2,7 @@
 <application>
     <name>Calendar</name>
     <!-- gettext('Calendar') -->   
-    <version>8.7</version>
+    <version>9.0</version>
     <order>15</order>
     <status>enabled</status>
     <tables>
index 7137139..f2f2e40 100644 (file)
     background-image:url(../../images/oxygen/32x32/devices/video-projector.png) !important;
 }
 
+.cal-year-view {
+    background-image:url(../../images/oxygen/16x16/actions/view-calendar-workweek.png) !important;
+}
+.x-btn-medium .cal-year-view {
+    background-image:url(../../images/oxygen/22x22/actions/view-calendar-workweek.png) !important;
+}
+.x-btn-large .cal-year-view {
+    background-image:url(../../images/oxygen/32x32/actions/view-calendar-workweek.png) !important;
+}
+
 .cal-attendee-type-resource {
     background-image:url(../../images/oxygen/16x16/devices/video-projector.png) !important;
     background-position:2px 2px;
index a7364ed..a698456 100644 (file)
@@ -177,10 +177,18 @@ div.page
 
 .cal-print-monthview .cal-print-daysview-day-untilseperator,
 .cal-print-monthview .cal-print-daysview-day-endtime {
-<<<<<<< HEAD
     display: none;
 }
-=======
-       display: none;
+
+/* ------------ yearview ------------ */
+.cal-print-yearview .cal-print-daycell {
+    width: 8%;
+    padding: 0px;
+    padding-left: 1px;
+    border: 1px solid #d0d0d0;
+}
+.cal-print-rowheader {
+    width: 1% !important;
 }
->>>>>>> 2013.10
+
+
diff --git a/tine20/Calendar/css/yearviewpanel.css b/tine20/Calendar/css/yearviewpanel.css
new file mode 100644 (file)
index 0000000..7c80277
--- /dev/null
@@ -0,0 +1,173 @@
+.cal-yearview {
+    color:black;
+    font-family:arial,tahoma,helvetica,sans-serif;
+    font-size:11px;
+    font-size-adjust:none;
+    font-style:normal;
+    font-variant:normal;
+    font-weight:normal;
+    line-height:normal;
+    white-space:nowrap;
+}
+
+.cal-yearview-inner th {
+    border-top: none;
+    text-align: center;
+    vertical-align: middle;
+}
+.cal-yearview-daycell {
+    vertical-align: top;
+    border-right: 1px solid #E1E1E1;
+    border-top: 1px solid #E1E1E1;
+    overflow: hidden;
+}
+
+.cal-yearview-daycell:hover {
+    background: #CDDCEF url(../../library/ExtJS/resources/images/default/toolbar/bg.gif) repeat-x scroll left top;
+    /*cursor: pointer; see ticket #1484*/
+}
+
+.cal-yearview-inner-header,
+cal-yearview-wkcell-header {
+    position: relative;
+    background:#F9F9F9 url(../../library/ExtJS/resources/images/default/grid/grid3-hrow.gif) repeat-x scroll 0 top;
+    height: 23px;
+}
+
+.cal-yearview-daycell {
+    position: relative;
+/*    width: 100%;*/
+}
+
+.cal-yearview-daycell-saturday {
+    background-color:#dddddd; 
+}
+
+.cal-yearview-daycell-sunday {
+    background-color:#bbbbbb; 
+}
+
+.cal-yearview-daycell-date {
+    padding: 2px 5px;
+    border-collapse: separate;
+    color: #233D6D;
+    /*cursor: pointer; see ticket #1484*/
+    float: left;
+    text-align: left;
+}
+
+.cal-yearview-daybody {
+    overflow: hidden;
+    float: left;
+    height: 100%;
+}
+
+/*********************************** Events ***********************************/
+.cal-yearview-eventslice {
+/*    height: 18px;*/
+    overflow: hidden;
+    float: left;
+    height: 100%;
+}
+
+.cal-yearview-event {
+    margin: 0px 0px;
+/*    width: 100%;*/
+    cursor: pointer;
+    border-radius: 8px;
+    margin: 0px 1px 0px 1px;
+    height: 100%;
+    line-height: 100%;
+}
+
+.cal-yearview-eventslice .cal-yearview-event-editgrant {
+    cursor: move;
+}
+
+.cal-yearview-event-summary {
+    white-space: nowrap;
+    overflow: hidden;
+}
+
+.ext-gecko .cal-yearview-alldayevent {
+    -moz-border-radius: 8px;
+}
+
+.ext-safari .cal-yearview-alldayevent {
+    -webkit-border-radius: 8px;
+}
+
+/** todo: add khtml switch **/
+.cal-yearview-alldayevent {
+    -khtml-border-radius: 8px;
+}
+
+.cal-yearview-event-croptop {
+/*    margin-left: 0px;
+    border-left-style: none;*/
+    border-top-left-radius: 0px;
+    border-top-right-radius: 0px;
+}
+/*
+.ext-gecko .cal-yearview-alldayevent-cropleft {
+    -moz-border-radius-topleft: 0px;
+    -moz-border-radius-bottomleft: 0px;
+}
+
+.ext-safari .cal-yearview-alldayevent-cropleft {
+    -webkit-border-top-left-radius: 0px;
+    -webkit-border-bottom-left-radius: 0px;
+}
+
+.cal-yearview-alldayevent-cropleft {
+    -khtml-border-top-left-radius: 0px;
+    -khtml-border-bottom-left-radius: 0px;
+}
+*/
+.cal-yearview-event-cropbottom {
+/*    margin-right: 0px;
+    background-image: none;
+    border-right-style: hidden;*/
+    border-bottom-left-radius: 0px;
+    border-bottom-right-radius: 0px;
+}
+/*
+.ext-gecko .cal-yearview-alldayevent-cropright {
+    -moz-border-radius-topright: 0px;
+    -moz-border-radius-bottomright: 0px;
+}
+
+.ext-safari .cal-yearview-alldayevent-cropright {
+    -webkit-border-top-right-radius: 0px;
+    -webkit-border-bottom-right-radius: 0px;
+}
+
+.cal-yearview-alldayevent-cropright {
+    -khtml-border-top-right-radius: 0px;
+    -khtml-border-bottom-right-radius: 0px;
+}
+*/
+
+.cal-yearview-alldayevent-summary {
+    white-space: nowrap;
+    overflow: hidden;
+    min-height: 12px;
+    /*overflow: visible; */
+}
+
+.cal-yearview-event-info-hide {
+    display: none;
+}
+
+.cal-yearview-daypreviewbox .cal-yearview-event-info-hide {
+    display: block;
+}
+
+/******************************* daypreviewbox ********************************/
+.cal-yearview-daypreviewbox {
+    position: absolute;
+    border-bottom: 1px solid #E1E1E1;
+    overflow-y: auto;
+    overflow-x: hidden;
+    z-index: 5000;
+}
index 26de21a..a1bccab 100644 (file)
@@ -477,3 +477,23 @@ Tine.Calendar.MonthViewEventUI = Ext.extend(Tine.Calendar.EventUI, {
         }
     }
 });
+
+Tine.Calendar.YearViewEventUI = Ext.extend(Tine.Calendar.EventUI, {
+    onSelectedChange: function(state){
+        Tine.Calendar.YearViewEventUI.superclass.onSelectedChange.call(this, state);
+        if (state){
+            this.addClass('cal-yearview-active');
+            this.setStyle({
+                'background-color': this.color,
+                'color':            (this.colorSet) ? this.colorSet.text : '#000000'
+            });
+            
+        } else {
+            this.removeClass('cal-yearview-active');
+            this.setStyle({
+                'background-color': this.is_all_day_event ? this.bgColor : '',
+                'color':            this.is_all_day_event ? '#000000' : this.color
+            });
+        }
+    }
+});
index 6994e0d..7f664c6 100644 (file)
@@ -57,7 +57,7 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
      */
     defaultPrintMode: 'sheet',
     
-    periodRe: /^(day|week|month)/i,
+    periodRe: /^(day|week|month|year)/i,
     presentationRe: /(sheet|grid)$/i,
     
     calendarPanels: {},
@@ -292,6 +292,15 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
             enableToggle: true,
             toggleGroup: 'Calendar_Toolbar_tgViews'
         });
+        this.showYearView = new Ext.Toolbar.Button({
+            pressed: String(this.activeView).match(/^year/i),
+            text: this.app.i18n._('Year'),
+            iconCls: 'cal-year-view',
+            xtype: 'tbbtnlockedtoggle',
+            handler: this.changeView.createDelegate(this, ["year"]),
+            enableToggle: true,
+            toggleGroup: 'Calendar_Toolbar_tgViews'
+        });
         
        this.action_import = new Ext.Action({
             requiredGrant: 'addGrant',
@@ -313,7 +322,11 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
             this.showWeekView,
             this.showMonthView
         ];
-        
+
+        if (this.app.featureEnabled('featureYearView')) {
+            this.changeViewActions.push(this.showYearView);
+        }
+
         this.recordActions = [
             this.action_import,
             this.action_editInNewWindow,
@@ -1660,6 +1673,12 @@ Tine.Calendar.MainScreenCenterPanel = Ext.extend(Ext.Panel, {
                             period: tbar.getPeriod()
                         });
                         break;
+                    case 'yearSheet':
+                        view = new Tine.Calendar.YearView({
+                            store: store,
+                            period: tbar.getPeriod()
+                        });
+                        break;
                     default:
                     case 'weekSheet':
                         view = new Tine.Calendar.DaysView({
index 5c4e6b9..2c0ec34 100644 (file)
@@ -453,3 +453,73 @@ Tine.Calendar.PagingToolbar.MonthPeriodPicker = Ext.extend(Tine.Calendar.PagingT
         };
     }
 });
+
+/**
+ * @class Tine.Calendar.PagingToolbar.YearPeriodPicker
+ * @extends Tine.Calendar.PagingToolbar.AbstractPeriodPicker
+ * @constructor
+ */
+Tine.Calendar.PagingToolbar.YearPeriodPicker = Ext.extend(Tine.Calendar.PagingToolbar.AbstractPeriodPicker, {
+    init: function() {
+        this.label = new Ext.form.Label({
+            text: Tine.Tinebase.appMgr.get('Calendar').i18n._('Year'),
+            style: 'padding-right: 3px'
+        });
+        this.field = new Ext.form.TextField({
+            value: this.tb.dtStart.format('Y'),
+            width: 40,
+            cls: "x-tbar-page-number",
+            listeners: {
+                scope: this,
+                specialkey: this.onSelect,
+                blur: this.onSelect
+            }
+        });
+    },
+    onSelect: function(field, e) {
+        if (e && e.getKey() == e.ENTER) {
+            return field.blur();
+        }
+        var diff = field.getValue() - this.dtStart.format('Y'); 
+        if (diff !== 0) {
+            this.update(this.dtStart.add(Date.YEAR, diff ))
+            this.fireEvent('change', this, 'year', this.getPeriod());
+        }
+        
+    },
+    update: function(dtStart) {
+        this.dtStart = dtStart.clone();
+        if (this.field && this.field.rendered) {
+            this.field.setValue(dtStart.format('Y'));
+        }
+    },
+    render: function() {
+        this.tb.addField(this.label);
+        this.tb.addField(this.field);
+    },
+    hide: function() {
+        this.label.hide();
+        this.field.hide();
+    },
+    show: function() {
+        this.label.show();
+        this.field.show();
+    },
+    next: function() {
+        this.dtStart = this.dtStart.add(Date.YEAR, 1);
+        this.update(this.dtStart);
+    },
+    prev: function() {
+        this.dtStart = this.dtStart.add(Date.YEAR, -1);
+        this.update(this.dtStart);
+    },
+    getPeriod: function() {
+        var from = Date.parseDate(this.dtStart.format('Y') + '-01-01 00:00:00', Date.patterns.ISO8601Long);
+        return {
+            from: from,
+            until: from.add(Date.YEAR, 1)
+        };
+    }
+});
+
+
index 85cc20f..a590377 100644 (file)
@@ -37,7 +37,7 @@ Tine.Calendar.Printer.BaseRenderer = Ext.extend(Ext.ux.Printer.BaseRenderer, {
         return ['<table style="', this.titleStyle, '"><tr><th class="cal-print-title">', this.extraTitle,  this.getTitle(view), '</th></tr></table>'].join('');
     },
     
-    generateCalRows: function(days, numCols, alignHorizontal) {
+    generateCalRows: function(days, numCols, alignHorizontal, hasRowHeader) {
         var row, col, cellsHtml, idx,
             numRows = Math.ceil(days.length/numCols),
             rowsHtml = '';
@@ -48,7 +48,8 @@ Tine.Calendar.Printer.BaseRenderer = Ext.extend(Ext.ux.Printer.BaseRenderer, {
             
             for (col=0; col<numCols; col++) {
                 idx = alignHorizontal ? row*numCols + col: col*numRows + row;
-                cellsHtml += String.format('<td class="cal-print-daycell" style="vertical-align: top;">{0}</td>', days[idx] || '');
+                rowheader = hasRowHeader && col == 0 ? 'cal-print-rowheader' : '';
+                cellsHtml += String.format('<td class="cal-print-daycell ' + rowheader + '" style="vertical-align: top;">{0}</td>', days[idx] || '');
             }
             
             rowsHtml += String.format('<tr class="cal-print-dayrow" style="height: {1}mm">{0}</tr>', cellsHtml, this.paperHeight/numRows);
diff --git a/tine20/Calendar/js/Printer/YearView.js b/tine20/Calendar/js/Printer/YearView.js
new file mode 100644 (file)
index 0000000..58475ff
--- /dev/null
@@ -0,0 +1,60 @@
+Tine.Calendar.Printer.YearViewRenderer = Ext.extend(Tine.Calendar.Printer.BaseRenderer, {
+    paperHeight: 155,
+
+    generateBody: function(view) {
+        var body = [];
+        
+        // try to force landscape -> opera only atm...
+        body.push('<style type="text/css">', 
+            '@page {',
+                'size:landscape',
+            '}',
+            '@media print {thead {display: table-header-group;}}',
+        '</style>');
+        
+       var monthNames = [];
+        monthNames.push("<th class='cal-print-yearview-daycell'><span></span></th>");
+        for(var i = 0; i < 12; i++){
+           monthNames.push("<th class='cal-print-yearview-daycell'><span>", view.monthNames[i], "</span></th>");
+        }
+        var daysHtml = [];
+       for(i=0; i< view.dayCells.length; i++)
+        {
+            if(i %12 == 0)
+                daysHtml.push(i/12+1);
+
+            celltext = view.dayCells[i].innerText;
+            if(celltext.length > 3 )
+                daysHtml.push(celltext.substring(celltext.indexOf("\n")));
+            else
+                daysHtml.push("");
+        }
+
+        body.push(
+        '<table class="cal-print-yearview">',
+            '<thead>',
+                '<tr><th colspan="13" class="cal-print-title">', this.getTitle(view), '</th></tr>',
+                '<tr>', monthNames.join("\n"), '</tr>',
+
+            '</thead>',
+            '<tbody>',
+                this.generateCalRows(daysHtml, 13, true, true),
+            '</tbody>');
+            
+        return body.join("\n");
+
+    },
+    
+    getTitle: function(view) {
+        return view.dateMesh[10].format('Y');
+    },
+    
+    dayHeadersTpl: new Ext.XTemplate(
+        '<tr>',
+            '<tpl for=".">',
+                '<th>\{{dataIndex}\}</th>',
+            '</tpl>',
+        '</tr>'
+    )
+});
diff --git a/tine20/Calendar/js/YearView.js b/tine20/Calendar/js/YearView.js
new file mode 100644 (file)
index 0000000..283dd81
--- /dev/null
@@ -0,0 +1,910 @@
+/* 
+ * Tine 2.0
+ * 
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Nico Hessler <tine20@nico-hessler.de>
+ * @copyright   Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de)
+ */
+
+Ext.ns('Tine.Calendar');
+
+/**
+ * @namespace Tine.Calendar
+ * @class Tine.Calendar.YearView
+ * @extends Ext.util.Observable
+ * @constructor
+ * @param {Object} config
+ */
+Tine.Calendar.YearView = function(config){
+    Ext.apply(this, config);
+    Tine.Calendar.YearView.superclass.constructor.call(this);
+    
+    this.printRenderer = Tine.Calendar.Printer.YearViewRenderer;
+    
+    this.addEvents(
+        /**
+         * @event click
+         * fired if an event got clicked
+         * @param {Tine.Calendar.Model.Event} event
+         * @param {Ext.EventObject} e
+         */
+        'click',
+        /**
+         * @event contextmenu
+         * fired if an event got contextmenu 
+         * @param {Ext.EventObject} e
+         */
+        'contextmenu',
+        /**
+         * @event dblclick
+         * fired if an event got dblclicked
+         * @param {Tine.Calendar.Model.Event} event
+         * @param {Ext.EventObject} e
+         */
+        'dblclick',
+        /**
+         * @event changeView
+         * fired if user wants to change view
+         * @param {String} requested view name
+         * @param {mixed} start param of requested view
+         */
+        'changeView',
+        /**
+         * @event changePeriod
+         * fired when period changed
+         * @param {Object} period
+         */
+        'changePeriod',
+        /**
+         * @event addEvent
+         * fired when a new event got inserted
+         * 
+         * @param {Tine.Calendar.Model.Event} event
+         */
+        'addEvent',
+        /**
+         * @event updateEvent
+         * fired when an event go resised/moved
+         * 
+         * @param {Tine.Calendar.Model.Event} event
+         */
+        'updateEvent'
+    );
+};
+
+Ext.extend(Tine.Calendar.YearView, Ext.Container, {
+    /**
+     * @cfg {Date} startDate
+     * start date
+     */
+    startDate: new Date().clearTime(),
+    /**
+     * @cfg {String} newEventSummary
+     * _('New Event')
+     */
+    newEventSummary: 'New Event',
+    /**
+     * @cfg String moreString
+     * _('{0} more...')
+     */
+    moreString: '{0} more...',
+    /**
+     * @cfg {Array} monthNames
+     * An array of textual month names which can be overriden for localization support (defaults to Date.monthNames)
+     */
+    monthNames : Date.monthNames,
+    /**
+     * @cfg {Boolean} denyDragOnMissingEditGrant
+     * deny drag action if edit grant for event is missing
+     */
+    denyDragOnMissingEditGrant: true,
+    /**
+     * @property {Tine.Calendar.Model.Event} activeEvent
+     * @private
+     */
+    activeEvent: null,
+    /**
+     * @private {Date} toDay
+     */
+    toDay: null,
+    /**
+     * @private {Array} dateMesh
+     */
+    dateMesh: null,
+    /**
+     * @private {Tine.Calendar.ParallelEventsRegistry} parallelEventsRegistry
+     */
+    parallelEventsRegistry: null,
+    
+    cls: "cal-yearview",
+    
+    /**
+     * @private
+     */
+    afterRender: function() {
+        Tine.Calendar.YearView.superclass.afterRender.apply(this, arguments);
+        
+        this.initElements();
+        
+        this.getSelectionModel().init(this);
+        
+        this.mon(this.el, 'mousedown', this.onMouseDown, this);
+        this.mon(this.el, 'click', this.onClick, this);
+        this.mon(this.el, 'contextmenu', this.onContextMenu, this);
+        this.mon(this.el, 'keydown', this.onKeyDown, this);
+        
+        this.initDragZone();
+        this.initDropZone();
+        
+        this.updatePeriod({from: this.period.from});
+        
+        if (this.store.getCount()) {
+            this.onLoad.apply(this);
+        }
+        
+        this.rendered = true;
+    },
+    
+    /**
+     * @private calculates mesh of dates for month this.startDate is in
+     */
+    calcDateMesh: function() {
+        var mesh = [];
+        var d = Date.parseDate(this.startDate.format('Y') + '-01-01 00:00:00', Date.patterns.ISO8601Long);
+
+        for (var day=0; day<31; day++) {
+            for (var month=0; month<12; month++) {
+                var cell = d.add(Date.MONTH, month).add(Date.DAY, day);
+                if(cell.getMonth() == month) {
+                    mesh.push(cell.clone());
+                }
+                else {
+                    mesh.push(null);
+                }
+            }
+        }
+        this.dateMesh = mesh;
+    },
+    
+    /**
+     * gets currentlcy active event
+     * 
+     * @return {Tine.Calendar.Model.Event} event
+     */
+    getActiveEvent: function() {
+        return this.activeEvent;
+    },
+    
+    /**
+     * returns index of dateCell given date is in
+     * @param {Date} date
+     */
+    getDayCellIndex: function(date) {
+        return date.getMonth() + (date.getDate() - 1) * 12;
+    },
+    
+    /**
+     * @private returns a child div in requested position
+     * 
+     * @param {dom} dayCell
+     * @param {Number} pos
+     * @return {dom}
+     */
+    getEventSlice: function(dayCell, pos) {
+        pos = Math.abs(pos);
+        
+        for (var i=dayCell.childNodes.length; i<=pos; i++) {
+            Ext.DomHelper.insertAfter(dayCell.lastChild, '<div class="cal-yearview-eventslice"/>');
+        }
+        
+        // make sure cell is empty
+        while (dayCell.childNodes[pos].innerHTML) {
+            pos++;
+            
+            if (pos > dayCell.childNodes.length -1) {
+                Ext.DomHelper.insertAfter(dayCell.lastChild, '<div class="cal-yearview-eventslice"/>');
+            }
+        }
+        
+        return dayCell.childNodes[pos];
+    },
+    
+    /**
+     * returns period of currently displayed month
+     * @return {Object}
+     */
+    getPeriod: function() {
+        // happens if month view is rendered first
+        if (! this.dateMesh) {
+            this.calcDateMesh();
+        }
+        
+        return {
+            from: this.dateMesh[0],
+            until: this.dateMesh[this.dateMesh.length -1].add(Date.DAY, 1)
+        };
+    },
+    
+    getSelectionModel: function() {
+        return this.selModel;
+    },
+    
+    getTargetDateTime: function(e) {
+        var target = e.getTarget('td.cal-yearview-daycell', 3);
+        
+        if (target) {
+            var dateIdx = this.dayCells.indexOf(target);
+            var date = this.dateMesh[this.dayCells.indexOf(target)];
+        
+            // set some default time:
+            date.add(Date.HOUR, 10);
+            return date;
+        }
+    },
+    
+    getTargetEvent: function(e) {
+        var target = e.getTarget('div.cal-yearview-event', 10);
+        
+        if (target) {
+            var parts = target.id.split(':');
+            var event = this.store.getById(parts[1]);
+        }
+        
+        return event;
+    },
+    
+    /**
+     * init month view
+     */
+    initComponent: function() {
+        this.app = Tine.Tinebase.appMgr.get('Calendar');
+        this.newEventSummary =  this.app.i18n._hidden(this.newEventSummary);
+        this.calWeekString   =  this.app.i18n._hidden(this.calWeekString);
+        this.moreString      =  this.app.i18n._hidden(this.moreString);
+        
+        // redefine this props in case ext translations got included after this component
+        this.monthNames = Date.monthNames;
+        this.dayNames   = Date.dayNames;
+        
+        this.initData(this.store);
+        this.initTemplates();
+        
+        if (! this.selModel) {
+            this.selModel = this.selModel || new Tine.Calendar.EventSelectionModel();
+        }
+        Tine.Calendar.YearView.superclass.initComponent.apply(this, arguments);
+    },
+    
+    /**
+     * @private
+     * @param {Ext.data.Store} ds
+     */
+    initData : function(ds){
+        if(this.store){
+            this.store.un("load", this.onLoad, this);
+            this.store.un("beforeload", this.onBeforeLoad, this);
+            this.store.un("add", this.onAdd, this);
+            this.store.un("remove", this.onRemove, this);
+            this.store.un("update", this.onUpdate, this);
+        }
+        if(ds){
+            ds.on("load", this.onLoad, this);
+            ds.on("beforeload", this.onBeforeLoad, this);
+            ds.on("add", this.onAdd, this);
+            ds.on("remove", this.onRemove, this);
+            ds.on("update", this.onUpdate, this);
+        }
+        this.store = ds;
+    },
+    
+    /**
+     * @private
+     */
+    initDragZone: function() {
+        this.dragZone = new Ext.dd.DragZone(this.el, {
+            ddGroup: 'cal-event',
+            view: this,
+            scroll: false,
+            
+            getDragData: function(e) {
+                var eventEl = e.getTarget('div.cal-yearview-event', 10);
+                if (eventEl) {
+                    var parts = eventEl.id.split(':');
+                    var event = this.view.store.getById(parts[1]);
+                    
+                    // don't allow dragging with missing edit grant
+                    if (this.view.denyDragOnMissingEditGrant && ! event.get('editGrant')) {
+                        return false;
+                    }
+                    
+                    // we need to clone an event with summary in
+                    var d = Ext.get(event.ui.domIds[0]).dom.cloneNode(true);
+                    
+                    var width = Ext.fly(eventEl).getWidth() * event.ui.domIds.length;
+                    
+                    Ext.fly(d).setWidth(width);
+                    Ext.fly(d).setOpacity(0.5);
+                    d.id = Ext.id();
+                    
+                    return {
+                        scope: this.view,
+                        sourceEl: eventEl,
+                        event: event,
+                        ddel: d,
+                        selections: this.view.getSelectionModel().getSelectedEvents()
+                    }
+                }
+            },
+            
+            getRepairXY: function(e, dd) {
+                Ext.fly(this.dragData.sourceEl).setOpacity(1, 1);
+                return Ext.fly(this.dragData.sourceEl).getXY();
+            }
+        });
+    },
+    
+    initDropZone: function() {
+        this.dd = new Ext.dd.DropZone(this.el.dom, {
+            ddGroup: 'cal-event',
+            
+            notifyOver : function(dd, e, data) {
+                var target = e.getTarget('td.cal-yearview-daycell', 3);
+                var event = data.event;
+                
+                // we dont support multiple dropping yet
+                if (event) {
+                    data.scope.getSelectionModel().select(event);
+                }
+                return target && event && event.get('editGrant') ? 'cal-daysviewpanel-event-drop-ok' : 'cal-daysviewpanel-event-drop-nodrop';
+            },
+            
+            notifyDrop : function(dd, e, data) {
+                var v = data.scope;
+                
+                var target = e.getTarget('td.cal-yearview-daycell', 3);
+                var targetDate = v.dateMesh[v.dayCells.indexOf(target)];
+                
+                if (targetDate) {
+                   var event = data.event;
+                    
+                    var diff = (targetDate.getTime() - event.get('dtstart').clearTime(true).getTime()) / Date.msDAY;
+                    if (! diff  || ! event.get('editGrant')) {
+                        return false;
+                    }
+                    
+                    event.beginEdit();
+                    event.set('dtstart', event.get('dtstart').add(Date.DAY, diff));
+                    event.set('dtend', event.get('dtend').add(Date.DAY, diff));
+                    event.endEdit();
+                    
+                    v.fireEvent('updateEvent', event);
+                }
+                
+                return !!targetDate;
+            }
+        });
+    },
+    
+    /**
+     * @private
+     */
+    initElements: function() {
+        var E = Ext.Element;
+
+        this.focusEl = new E(this.el.dom.firstChild);
+        
+        this.mainHd = new E(this.el.dom.lastChild.firstChild);
+        this.mainBody = new E(this.el.dom.lastChild.lastChild);
+        
+        this.dayCells = Ext.DomQuery.select('td[class=cal-yearview-daycell]', this.mainBody.dom);
+    },
+    
+    /**
+     * inits all tempaltes of this view
+     */
+    initTemplates: function() {
+        var ts = this.templates || {};
+        
+        ts.event = new Ext.XTemplate(
+            '<div id="{id}" class="cal-yearview-event {extraCls}" style="background-color: {bgColor};"">' +
+                //'<div class="cal-yearview-event-summary"> {[Ext.util.Format.htmlEncode(values.summary)]}</div>' +
+                '{[Ext.util.Format.htmlEncode(values.summary)]}' +
+            '</div>'
+        );
+        
+        for(var k in ts){
+            var t = ts[k];
+            if(t && typeof t.compile == 'function' && !t.compiled){
+                t.disableFormats = true;
+                t.compile();
+            }
+        }
+
+        this.templates = ts;
+    },
+    
+    /**
+     * @private
+     * @param {Tine.Calendar.Model.Event} event
+     */
+    insertEvent: function(event) {
+        event.ui = new Tine.Calendar.YearViewEventUI(event);
+        //event.ui.render(this);
+        
+        var dtStart = event.get('dtstart');
+        var startCellNumber = this.getDayCellIndex(dtStart);
+        
+        var dtEnd = event.get('dtend');
+        // 00:00 in users timezone is a spechial case where the user expects
+        // something like 24:00 and not 00:00
+        if (dtEnd.format('H:i') == '00:00') {
+            dtEnd = dtEnd.add(Date.MINUTE, -1);
+        }
+        var endCellNumber = this.getDayCellIndex(dtEnd);
+        
+        // skip out of range events
+        if (endCellNumber < 0 || startCellNumber >= this.dateMesh.length) {
+            return;
+        }
+        
+        var pos = this.parallelEventsRegistry.getPosition(event);
+        
+        // save some layout info
+        event.ui.colorSet = event.colorSet = Tine.Calendar.colorMgr.getColor(event);
+        event.ui.color = event.ui.colorSet.color;
+        event.ui.bgColor = event.ui.colorSet.light;
+        
+        var data = {
+            startTime: dtStart.format('H:i'),
+            summary: event.get('summary'),
+            color: event.ui.color,
+            bgColor: event.ui.bgColor,
+            width: '100%'
+        };
+        
+        for (var i=Math.max(startCellNumber, 0); i<=Math.min(endCellNumber, this.dayCells.length-1) ; i=i+12) {
+            var col = i%12, row = Math.floor(i/12);
+            //var row = i%12, col = Math.floor(i/12);
+            
+            data.id = Ext.id() + '-event:' + event.get('id');
+            event.ui.domIds.push(data.id);
+                
+            var tmpl = this.templates.event;
+            data.extraCls = event.get('editGrant') ? 'cal-yearview-event-editgrant' : '';
+            data.extraCls += ' cal-status-' + event.get('status');
+             
+            if (data.showInfo && startCellNumber != endCellNumber) {
+                var cols = (row == Math.floor(endCellNumber/12) ? endCellNumber%12 : 11) - col +1;
+                data.width = 100 * cols + '%';
+            }
+
+            if (i > startCellNumber) {
+                data.extraCls += ' cal-yearview-event-croptop';
+            }
+            if (i < endCellNumber) {
+                data.extraCls += ' cal-yearview-event-cropbottom';
+            }
+            
+            var posEl = this.getEventSlice(this.dayCells[i].lastChild, pos);
+            var eventEl = tmpl.overwrite(posEl, data, true);
+            
+            if (event.dirty) {
+                eventEl.setOpacity(0.5);
+                
+                // the event was selected before
+                event.ui.onSelectedChange(true);
+            }
+        }
+    },
+    
+    onLayout: function() {
+        Tine.Calendar.YearView.superclass.onLayout.apply(this, arguments);
+        
+        if(!this.mainBody){
+            return; // not rendered
+        }
+        
+        var csize = this.container.getSize(true);
+        var vw = csize.width;
+        var vh = csize.height;
+        
+        var hsize = this.mainHd.getSize(true);
+        
+        this.dayCellsHeight = vh/31-2; 
+        this.dayCellsWidth = vw/12-25; 
+
+        var hdCels = this.mainHd.dom.firstChild.childNodes;
+        for (var i=0; i<hdCels.length; i++) {
+            Ext.get(hdCels[i]).setWidth(vw/12);
+        }
+        
+        for (var i=0; i<this.dayCells.length; i++) {
+            Ext.get(this.dayCells[i].lastChild).setSize(this.dayCellsWidth, Math.max(this.dayCellsHeight, 17));
+        }
+        
+        this.layoutDayCells();
+    },
+    
+    onDestroy: function() {
+        this.removeAllEvents();
+        this.initData(false);
+        this.purgeListeners();
+        
+        Tine.Calendar.YearView.superclass.onDestroy.apply(this, arguments);
+    },
+    
+    /**
+     * layouts the contents (sets 'more items marker')
+     */
+    layoutDayCells: function() {
+        for (var i=0; i<this.dayCells.length; i++) {
+            if (this.dayCells[i].lastChild.childNodes.length > 1) {
+                this.layoutDayCell(this.dayCells[i], true, true);
+            }
+        }
+    },
+    
+    /**
+     * layouts a single day cell
+     * 
+     * @param {dom} cell
+     * @param {Bool} hideOverflow
+     * @param {Bool} updateHeader
+     */
+    layoutDayCell: function(cell, hideOverflow, updateHeader) {
+        // clean empty slices
+        while (cell.lastChild.childNodes.length > 1 && cell.lastChild.lastChild.innerHTML == '') {
+            Ext.fly(cell.lastChild.lastChild).remove();
+        }
+        
+        Tine.log.debug('Tine.Calendar.YearView::layoutDayCell() - cell:');
+        Tine.log.debug(cell);
+
+        var items = cell.lastChild.childNodes.length;
+        
+        for (var j=0; j<items; j++) {
+            var eventEl = Ext.get(cell.lastChild.childNodes[j]);
+            
+            //eventEl[height > this.dayCellsHeight && hideOverflow ? 'hide' : 'show']();
+            eventEl.dom.style.width = this.dayCellsWidth / items ;
+            cell.lastChild.childNodes[j].style.width = this.dayCellsWidth / items ;
+
+        }
+        
+        
+        //return height;
+    },
+    
+    /**
+     * @private
+     */
+    onAdd : function(ds, records, index){
+        for (var i=0; i<records.length; i++) {
+            var event = records[i];
+            this.parallelEventsRegistry.register(event);
+            
+            var parallelEvents = this.parallelEventsRegistry.getEvents(event.get('dtstart'), event.get('dtend'));
+            
+            for (var j=0; j<parallelEvents.length; j++) {
+                this.removeEvent(parallelEvents[j]);
+                this.insertEvent(parallelEvents[j]);
+            }
+            
+            this.setActiveEvent(event);
+        }
+        
+        this.layoutDayCells();
+    },
+    
+    onClick: function(e, target) {
+        
+        // send click event anyway
+        var event = this.getTargetEvent(e);
+        if (event) {
+            this.fireEvent('click', event, e);
+            return;
+        }
+        
+        /** distinct click from dblClick **/
+        var now = new Date().getTime();
+        
+        if (now - parseInt(this.lastClickTime, 10) < 300) {
+            this.lastClickTime = now;
+            //e.stopEvent();
+            return;
+        }
+        
+        var dateTime = this.getTargetDateTime(e);
+        if (Math.abs(dateTime - now) < 100) {
+            this.lastClickTime = now;
+            return this.onClick.defer(400, this, [e, target]);
+        }
+        this.lastClickTime = now;
+        /** end distinct click from dblClick **/
+        
+        switch(target.className) {
+            case 'cal-yearview-monthheader-date':
+            case 'cal-yearview-monthheader-more':
+                var moreText = target.parentNode.firstChild.innerHTML;
+                if (! moreText) {
+                    return;
+                }
+                
+                //e.stopEvent();
+                this.zoomDayCell(target.parentNode.parentNode);
+                break;
+        }
+    },
+    
+    onContextMenu: function(e) {
+       //TODO reenable
+        //this.fireEvent('contextmenu', e);
+    },
+    
+    onKeyDown : function(e){
+        this.fireEvent("keydown", e);
+    },
+    
+    onBeforeLoad: function(store, options) {
+        if (! options.refresh) {
+            this.store.each(this.removeEvent, this);
+        }
+    },
+    
+    /**
+     * @private
+     */
+    onLoad : function(){
+        if(! this.rendered){
+            return;
+        }
+        
+        this.removeAllEvents();
+        
+        // create parallels registry
+        this.parallelEventsRegistry = new Tine.Calendar.ParallelEventsRegistry({
+            dtStart: this.dateMesh[0], 
+            dtEnd: this.dateMesh[this.dateMesh.length-1].add(Date.DAY, 1)/*.add(Date.SECOND, -1)*/,
+            granularity: 60*24
+        });
+        
+        // todo: sort generic?
+        this.store.fields = Tine.Calendar.Model.Event.prototype.fields;
+        this.store.sortInfo = {field: 'dtstart', direction: 'ASC'};
+        this.store.applySort();
+        
+        // calculate duration and parallels
+        this.store.each(function(event) {
+            this.parallelEventsRegistry.register(event);
+        }, this);
+        
+        this.store.each(this.insertEvent, this);
+        this.layoutDayCells();
+    },
+    
+    /**
+     * @private
+     */
+    onMouseDown: function(e, target) {
+        this.focusEl.focus();
+        this.mainBody.focus();
+        
+        // only unzoom if click is not in the area of the daypreviewbox
+        if (! e.getTarget('div.cal-yearview-daypreviewbox')) {
+            this.unZoom();
+        }
+    },
+    
+    /**
+     * @private
+     */
+    onRemove : function(ds, event, index, isUpdate){
+        this.parallelEventsRegistry.unregister(event);
+        this.removeEvent(event);
+        this.getSelectionModel().unselect(event);
+    },
+    
+    /**
+     * @private
+     */
+    onUpdate : function(ds, event){
+        // relayout original context
+        var originalDtstart = event.modified.hasOwnProperty('dtstart') ? event.modified.dtstart : event.get('dtstart');
+        var originalDtend = event.modified.hasOwnProperty('dtend') ? event.modified.dtend : event.get('dtend');
+            
+        var originalParallels = this.parallelEventsRegistry.getEvents(originalDtstart, originalDtend);
+        for (var j=0; j<originalParallels.length; j++) {
+            this.removeEvent(originalParallels[j]);
+        }
+        this.parallelEventsRegistry.unregister(event);
+        
+        var originalParallels = this.parallelEventsRegistry.getEvents(originalDtstart, originalDtend);
+        for (var j=0; j<originalParallels.length; j++) {
+            this.insertEvent(originalParallels[j]);
+        }
+        
+        
+        // relayout actual context
+        var parallelEvents = this.parallelEventsRegistry.getEvents(event.get('dtstart'), event.get('dtend'));
+        for (var j=0; j<parallelEvents.length; j++) {
+            this.removeEvent(parallelEvents[j]);
+        }
+        this.parallelEventsRegistry.register(event);
+        
+        var parallelEvents = this.parallelEventsRegistry.getEvents(event.get('dtstart'), event.get('dtend'));
+        for (var j=0; j<parallelEvents.length; j++) {
+            this.insertEvent(parallelEvents[j]);
+        }
+        
+        event.commit(true);
+        this.setActiveEvent(this.getActiveEvent());
+        this.layoutDayCells();
+    },
+    
+    /**
+     * print wrapper
+     */
+    print: function() {
+        var renderer = new this.printRenderer();
+        renderer.print(this);
+    },
+    
+    /**
+     * removes all events from dom
+     */
+    removeAllEvents: function() {
+        var els = Ext.DomQuery.filter(Ext.DomQuery.select('div[class^=cal-yearview-event]', this.mainBody.dom), 'div[class=cal-yearview-eventslice]', true);
+        for (var i=0; i<els.length; i++) {
+            Ext.fly(els[i]).remove();
+        }
+        
+        this.store.each(function(event) {
+            if (event.ui) {
+                event.ui.domIds = [];
+            }
+        });
+        this.layoutDayCells();
+    },
+    
+    /**
+     * removes a event from the dom
+     * @param {Tine.Calendar.Model.Event} event
+     */
+    removeEvent: function(event) {
+        if (! event) {
+            return;
+        }
+        
+        if (event == this.activeEvent) {
+            this.activeEvent = null;
+        }
+        
+        if (event.ui) {
+            event.ui.remove();
+        }
+    },
+    
+    /**
+     * renders the view
+     */
+    onRender: function(container, position) {
+        Tine.Calendar.YearView.superclass.onRender.apply(this, arguments);
+        
+        var m = [
+             '<a href="#" class="cal-yearviewpanel-focus" tabIndex="-1"></a>',
+             '<table class="cal-yearview-inner" cellspacing="0"><thead><tr class="cal-yearview-inner-header" height="23px">'
+         ];
+        for(var i = 0; i < 12; i++){
+            m.push("<th class='cal-yearview-monthcell'><span>", this.monthNames[i], "</span></th>");
+        }
+        m[m.length] = "</tr></thead><tbody><tr>";
+        for(var i = 0; i < 372; i++) {
+            if(i % 12 == 0 && i != 0){
+                m[m.length] = "</tr><tr>";
+            }
+            m[m.length] = 
+                '<td class="cal-yearview-daycell">' +
+//                    '<div class="cal-yearview-dayheader">' +
+//                        '<div class="cal-yearview-dayheader-more"></div>' +
+                        '<div class="cal-yearview-daycell-date"></div>' +
+//                    '</div>' +
+                    //'<div class="cal-yearview-daybody"><div class="cal-yearview-eventslice" /></div>' +
+                    '<div class="cal-yearview-daybody"><div class="cal-yearview-eventslice" /></div>' +
+                '</td>';
+        }
+        m.push('</tr></tbody></table>');
+        
+                
+        this.el.update(m.join(""));
+    },
+       
+    /**
+     * sets currentlcy active event
+     * 
+     * NOTE: active != selected
+     * @param {Tine.Calendar.Model.Event} event
+     */
+    setActiveEvent: function(event) {
+        this.activeEvent = event || null;
+    },
+    
+    updatePeriod: function(period) {
+        this.toDay = new Date().clearTime();
+        this.startDate = period.from;
+        this.calcDateMesh();
+        
+        var tbar = this.findParentBy(function(c) {return c.getTopToolbar()}).getTopToolbar();
+        if (tbar) {
+            tbar.periodPicker.update(this.startDate);
+            this.startDate = tbar.periodPicker.getPeriod().from;
+        }
+        
+        if (! this.rendered) return;
+        
+        // update dates and bg colors
+        var monthHeaders = Ext.DomQuery.select('div[class=cal-yearview-daycell-date]', this.mainBody.dom);
+        for(var i = 0; i < this.dateMesh.length; i++) {
+            if(this.dateMesh[i] != null && monthHeaders[i]) {
+                //clsToAdd = ((this.dateMesh[i].getMonth() == this.startDate.getMonth()) ? ' cal-yearview-daycell-valid' : ' cal-yearview-daycell-invalid');
+                clsToAdd = "";
+                clsToAdd = clsToAdd + ((this.dateMesh[i].getDay() == 0 ) ? ' cal-yearview-daycell-sunday' : '' );
+                clsToAdd = clsToAdd + ((this.dateMesh[i].getDay() == 6 ) ? ' cal-yearview-daycell-saturday' : '' );
+                if (this.dateMesh[i].getTime() == this.toDay.getTime()) {
+                    clsToAdd = clsToAdd + ' cal-yearview-daycell-today';
+                }
+                this.dayCells[i].setAttribute('class', 'cal-yearview-daycell ' + clsToAdd);
+                monthHeaders[i].innerHTML = this.dateMesh[i].format('j');
+            }
+        }
+       
+        this.onLayout();
+        this.fireEvent('changePeriod', period);
+    },
+    
+    unZoom: function() {
+        if (this.zoomCell) {
+            // this prevents reopen of cell on header clicks
+            this.lastClickTime = new Date().getTime();
+            
+            var cell = Ext.get(this.zoomCell);
+            var dayBodyEl = cell.last();
+            var height = cell.getHeight() - cell.first().getHeight();
+            dayBodyEl.scrollTo('top');
+            dayBodyEl.removeClass('cal-yearview-daypreviewbox');
+            dayBodyEl.setStyle('background-color', cell.getStyle('background-color'));
+            dayBodyEl.setStyle('border-top', 'none');
+            dayBodyEl.setHeight(height);
+            
+            
+            // NOTE: we need both setWidht statements, otherwise safari keeps scroller space
+            for (var i=0; i<dayBodyEl.dom.childNodes.length; i++) {
+                Ext.get(dayBodyEl.dom.childNodes[i]).setWidth(dayBodyEl.getWidth());
+                Ext.get(dayBodyEl.dom.childNodes[i]).setWidth(dayBodyEl.first().getWidth());
+            }
+            
+            this.layoutDayCell(this.zoomCell, true, true);
+            
+            this.zoomCell = false;
+        }
+        
+    },
+    
+    zoomDayCell: function(cell) {
+        this.zoomCell = cell;
+        
+        var dayBodyEl = Ext.get(cell.lastChild);
+        var box = dayBodyEl.getBox();
+        var bgColor = Ext.fly(cell).getStyle('background-color');
+        bgColor == 'transparent' ? '#FFFFFF' : bgColor
+        
+        dayBodyEl.addClass('cal-yearview-daypreviewbox');
+        dayBodyEl.setBox(box);
+        dayBodyEl.setStyle('background-color', bgColor);
+        dayBodyEl.setStyle('border-top', '1px solid ' + bgColor);
+        
+        var requiredHeight = this.layoutDayCell(cell, false, true) + 10;
+        var availHeight = this.el.getBottom() - box.y;
+        dayBodyEl.setHeight(Math.min(requiredHeight, availHeight));
+    }
+});
+Ext.reg('Tine.Calendar.YearView', Tine.Calendar.YearView);
index 24ce7b0..5df6a79 100644 (file)
@@ -28,4 +28,14 @@ class Courses_Setup_Update_Release8 extends Setup_Update_Abstract
         $this->setTableVersion('courses', 7);
         $this->setApplicationVersion('Courses', '8.1');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_1()
+    {
+        $this->setApplicationVersion('Courses', '9.0');
+    }
 }
\ No newline at end of file
index 2df6919..d435736 100644 (file)
@@ -2,7 +2,7 @@
 <application>
     <name>Courses</name>
     <!-- gettext('Courses') -->   
-    <version>8.1</version>
+    <version>9.0</version>
     <order>50</order>
     <status>enabled</status>
     <depends>
index 1d3a28b..8a17db6 100644 (file)
@@ -56,4 +56,14 @@ class Crm_Setup_Update_Release8 extends Setup_Update_Abstract
         Setup_Controller::getInstance()->createImportExportDefinitions(Tinebase_Application::getInstance()->getApplicationByName('Crm'));
         $this->setApplicationVersion('Crm', '8.3');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_3()
+    {
+        $this->setApplicationVersion('Crm', '9.0');
+    }
 }
index e4f4368..47605cf 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Crm</name>
-    <version>8.3</version>
+    <version>9.0</version>
     <order>20</order>
     <depends>
         <application>Admin</application>
index 90f6324..683963a 100644 (file)
@@ -4,7 +4,7 @@
  * @package     Crm
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Philipp Schuele <p.schuele@metaways.de>
- * @copyright   Copyright (c) 2009 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2009-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  */
  
@@ -93,20 +93,6 @@ Tine.Crm.LeadGridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
         }
         
         return a.join("\n");
-        
-        /*
-        getMailLink: function(email, felamimail) {
-                    if (! email) {
-                        return '';
-                    }
-                    
-                    var link = (felamimail) ? '#' : 'mailto:' + email;
-                    var id = Ext.id() + ':' + email;
-                    
-                    return '<a href="' + link + '" class="tinebase-email-link" id="' + id + '">'
-                        + Ext.util.Format.ellipsis(email, 18); + '</a>';
-                }
-         */
     },
     
     /**
@@ -146,25 +132,6 @@ Tine.Crm.LeadGridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
             id: 'id'
         });
         
-        /*
-        this.defaultPanel = this.getDefaultPanel();
-        this.leadDetailsPanel = this.getLeadGridDetailsPanel();
-        
-        this.cardPanel = new Ext.Panel({
-            layout: 'card',
-            border: false,
-            activeItem: 0,
-            items: [
-                this.defaultPanel,
-                this.leadDetailsPanel
-            ]
-        });
-        
-        this.items = [
-            this.cardPanel
-        ];
-        */
-        
         this.supr().initComponent.call(this);
     },
     
@@ -173,210 +140,20 @@ Tine.Crm.LeadGridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
      * 
      * @return {Ext.ux.display.DisplayPanel}
      * 
-     * TODO add legend?
+     * TODO add something useful here
      */
     getDefaultInfosPanel: function() {
         if (! this.defaultInfosPanel) {
             this.defaultInfosPanel = new Ext.ux.display.DisplayPanel({
                 layout: 'fit',
                 border: false,
-                items: [{
-                    layout: 'hbox',
-                    border: false,
-                    defaults:{
-                        margins:'0 5 0 0',
-                        padding: 2,
-                        style: {
-                            cursor: 'crosshair'
-                        },
-                        flex: 1,
-                        layout: 'ux.display',
-                        border: false
-                    },
-                    layoutConfig: {
-                        padding:'5',
-                        align:'stretch'
-                    },
-                    items: [{
-                        layoutConfig: {
-                            background: 'border',
-                            declaration: this.app.i18n._('Leadstates')
-                        },
-                        items: [{
-                            store: this.leadstatePiechartStore,
-                            xtype: 'piechart',
-                            dataField: 'total',
-                            categoryField: 'label'
-                        }]
-                    }, {
-                        layoutConfig: {
-                            background: 'border',
-                            declaration: this.app.i18n._('Leadsources')
-                        },
-                        items: [{
-                            store: this.leadsourcePiechartStore,
-                            xtype: 'piechart',
-                            dataField: 'total',
-                            categoryField: 'label'
-                        }]
-                    }, {
-                        layoutConfig: {
-                            background: 'border',
-                            declaration: this.app.i18n._('Leadtypes')
-                        },
-                        items: [{
-                            store: this.leadtypePiechartStore,
-                            xtype: 'piechart',
-                            dataField: 'total',
-                            categoryField: 'label'
-                        }]
-                    }]
-                }]
-                /*
-                    fieldLabel: this.app.i18n._('Leadstates'), // ??
-                    xtype: 'piechart',
-                    store: this.leadstatePiechartStore,
-                    dataField: 'total',
-                    categoryField: 'label',
-                    backgroundColor: '#eeeeee' // ??
-                    //extra styles get applied to the chart defaults
-                    extraStyle: {
-                        legend: {
-                            //display: 'right',
-                            display: 'top',
-                            padding: 5,
-                            font: {
-                                family: 'Tahoma',
-                                size: 8
-                            }
-                        }
-                    } 
-                */               
+                items: []
             });
         }
         
         return this.defaultInfosPanel;
     },
-    
-    /**
-     * fill the piechart stores (calls loadPiechartStore() for all piecharts)
-     */
-    setPiechartStores: function(getFromRequest) {
-        
-        if (! this.getDefaultInfosPanel().isVisible()) {
-            return;
-        }
-        
-        if (getFromRequest === false) {
-            var data = this.getCountFromSelection();
-        } else {
-            var data = {
-                leadstate: this.grid.store.proxy.jsonReader.jsonData.totalleadstates,
-                leadsource: this.grid.store.proxy.jsonReader.jsonData.totalleadsources,
-                leadtype: this.grid.store.proxy.jsonReader.jsonData.totalleadtypes
-            };
-        }
-        
-        //console.log(data);
-        
-        var storesConfig = [{
-            store: this.leadstatePiechartStore,
-            data: data.leadstate,
-            definitionsStore: Tine.Crm.LeadState.getStore(),
-            definitionsLabel: 'leadstate'
-        }, {
-            store: this.leadsourcePiechartStore,
-            data: data.leadsource,
-            definitionsStore: Tine.Crm.LeadSource.getStore(),
-            definitionsLabel: 'leadsource'
-        }, {
-            store: this.leadtypePiechartStore,
-            data: data.leadtype,
-            definitionsStore: Tine.Crm.LeadType.getStore(),
-            definitionsLabel: 'leadtype'
-        }];
-        
-        for (var i = 0; i < storesConfig.length; i++) {
-            this.loadPiechartStore(storesConfig[i]);
-        }
-    },
-    
-    /**
-     * get leadstzate/source/type count for charts from selection
-     * 
-     * @return {}
-     */
-    getCountFromSelection: function() {
-      
-        var result = {
-            leadstate: {},
-            leadsource: {},
-            leadtype: {}
-        };
-        
-        var selectedRows = this.grid.getSelectionModel().getSelections();
-        for (var i = 0; i < selectedRows.length; ++i) {
-            //console.log(selectedRows[i]);
-            if (! result.leadstate[selectedRows[i].get('leadstate_id')]) {
-                result.leadstate[selectedRows[i].get('leadstate_id')] = 1;
-            } else {
-                result.leadstate[selectedRows[i].get('leadstate_id')]++;
-            }
-
-            if (! result.leadsource[selectedRows[i].get('leadsource_id')]) {
-                result.leadsource[selectedRows[i].get('leadsource_id')] = 1;
-            } else {
-                result.leadsource[selectedRows[i].get('leadsource_id')]++;
-            }
 
-            if (! result.leadtype[selectedRows[i].get('leadtype_id')]) {
-                result.leadtype[selectedRows[i].get('leadtype_id')] = 1;
-            } else {
-                result.leadtype[selectedRows[i].get('leadtype_id')]++;
-            }
-        }
-        
-        return result;
-    },
-    
-    /**
-     * load data into piechart store
-     * 
-     * @param {} config
-     */
-    loadPiechartStore: function(config) {
-        try {
-            if (config.store.getCount() > 0) {
-                config.store.removeAll();
-            }
-            
-            // get records from defintion / grid store request
-            var records = [];
-            if (config.data) {
-                config.definitionsStore.each(function(definition) {
-                    if (config.data[definition.id]) {
-                        records.push(new config.store.recordType({
-                            id: definition.id,
-                            label: definition.get(config.definitionsLabel),
-                            total: config.data[definition.id]
-                        }, definition.id));
-                    }
-                }, this);
-            }
-            
-            // add new records
-            if (records.length > 0) {
-                config.store.add(records);
-            }
-        } catch (e) {
-            //console.log('error while setting ' + config.definitionsLabel + ' piechart data');
-            //console.log(e);
-            
-            // some error with the piechart occurred, try it again ...
-            this.loadPiechartStore.defer(500, this, [config]);
-        }
-    },
-    
     /**
      * get panel for multi selection aggregates/information
      * 
@@ -397,7 +174,6 @@ Tine.Crm.LeadGridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
     getSingleRecordPanel: function() {
         if (! this.singleRecordPanel) {
             this.singleRecordPanel = new Ext.ux.display.DisplayPanel ({
-                //xtype: 'displaypanel',
                 layout: 'fit',
                 border: false,
                 items: [{
@@ -532,7 +308,6 @@ Tine.Crm.LeadGridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
      * @param {Mixed} body
      */
     showDefault: function(body) {
-        this.setPiechartStores.defer(500, this, [true]);
     },
     
     /**
@@ -542,6 +317,5 @@ Tine.Crm.LeadGridDetailsPanel = Ext.extend(Tine.widgets.grid.DetailsPanel, {
      * @param {Mixed} body
      */
     showMulti: function(sm, body) {
-        this.setPiechartStores.defer(1000, this, [false]);
     }
 });
index b47d996..c9a27e5 100644 (file)
@@ -632,6 +632,7 @@ class Felamimail_Controller_Message extends Tinebase_Controller_Record_Abstract
                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
                     . ' Do not convert ' . $bodyPart->type . ' part to ' . $_contentType);
             }
+            $body = Felamimail_Message::replaceTargets($body);
             
             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
                 . ' Adding part ' . $partId . ' to message body.');
index c58c10b..b7eeeca 100644 (file)
@@ -108,7 +108,7 @@ class Felamimail_Controller_Message_Send extends Felamimail_Controller_Message
             list($originalMessageId, $partId) = explode('_', $originalMessageId);
         } else if (is_array($originalMessageId)) {
             if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
-                    . ' Something strange happened. original_id is an array: ' . print_($originalMessageId, true));
+                    . ' Something strange happened. original_id is an array: ' . print_r($originalMessageId, true));
             return;
         } else {
             $partId = NULL;
index 80588c9..2680e30 100644 (file)
@@ -214,7 +214,7 @@ class Felamimail_Message extends Zend_Mail_Message
             
         return $result;
     }
-    
+
     /**
      * replace uris with links
      *
@@ -249,6 +249,21 @@ class Felamimail_Message extends Zend_Mail_Message
     }
 
     /**
+     * replace targets in links
+     *
+     * @param string $_content
+     * @return string
+     */
+    public static function replaceTargets($_content) 
+    {
+        // uris
+        $pattern = "/target=[\'\"][^\'\"]*[\'\"]/";
+        $result = preg_replace($pattern, "target=\"_blank\"", $_content);
+        
+        return $result;
+    }
+
+    /**
      * create Felamimail message from Zend_Mail_Message
      * 
      * @param Zend_Mail_Message $_zendMailMessage
index 37593a7..454755e 100644 (file)
@@ -151,4 +151,14 @@ class Felamimail_Setup_Update_Release8 extends Setup_Update_Abstract
         ), $where);
         $this->setApplicationVersion('Felamimail', '8.4');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_4()
+    {
+        $this->setApplicationVersion('Felamimail', '9.0');
+    }
 }
index c26c26f..757527b 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Felamimail</name>
-    <version>8.4</version>
+    <version>9.0</version>
     <order>30</order>
     <status>enabled</status>
     <tables>
index a566f8c..09c867f 100644 (file)
@@ -30,20 +30,63 @@ Tine.Felamimail.GridPanelHook = function(config) {
     
     // NOTE: due to the action updater this action is bound the the adb grid only!
     this.composeMailAction = new Ext.Action({
-        actionType: 'add',
-        text: this.app.i18n._('Compose email'),
-        iconCls: this.app.getIconCls(),
-        disabled: true,
+      actionType: 'add',
+      text: this.app.i18n._('Compose email'),
+      iconCls: this.app.getIconCls(),
+      disabled: true,
+      scope: this,
+      actionUpdater: this.updateAction,
+      handler: this.onComposeEmailTO,
+      allowMultiple: true,
+      listeners: {
         scope: this,
-        actionUpdater: this.updateAction,
-        handler: this.onComposeEmail,
-        listeners: {
-            scope: this,
-            render: this.onRender
-        }
+        render: this.onRender
+      },
+      menu: {
+        items: [
+          this.composeMailActionTO = new Ext.Action({
+              actionType: 'add',
+              text: this.app.i18n._('To'),
+              iconCls: this.app.getIconCls(),
+              disabled: true,
+              scope: this,
+              actionUpdater: this.updateAction,
+              handler: this.onComposeEmailTO,
+              listeners: {
+                  scope: this,
+                  render: this.onRender
+              }
+          }),
+          this.composeMailActionCC = new Ext.Action({
+              actionType: 'add',
+              text: this.app.i18n._('CC'),
+              iconCls: this.app.getIconCls(),
+              disabled: true,
+              scope: this,
+              actionUpdater: this.updateAction,
+              handler: this.onComposeEmailCC,
+              listeners: {
+                  scope: this,
+                  render: this.onRender
+              }
+          }),
+          this.composeMailActionBCC = new Ext.Action({
+              actionType: 'add',
+              text: this.app.i18n._('BCC'),
+              iconCls: this.app.getIconCls(),
+              disabled: true,
+              scope: this,
+              actionUpdater: this.updateAction,
+              handler: this.onComposeEmailBCC,
+              listeners: {
+                  scope: this,
+                  render: this.onRender
+              }
+          })
+        ]
+      }
     });
-    
-    this.composeMailBtn = Ext.apply(new Ext.Button(this.composeMailAction), {
+    this.composeMailBtn = Ext.apply(new Ext.Button(this.composeMailActionTO), {
         scale: 'medium',
         rowspan: 2,
         iconAlign: 'top'
@@ -68,13 +111,27 @@ Ext.apply(Tine.Felamimail.GridPanelHook.prototype, {
      * @type String
      */
     foreignAppName: null,
+     
+    /**
+     * @property composeMailAction
+     * @type Tine.widgets.ActionUpdater
+     * @private
+     */
+    composeMailActionTO: null,
+    
+    /**
+     * @property composeMailAction
+     * @type Tine.widgets.ActionUpdater
+     * @private
+     */
+    composeMailActionCC: null,
     
     /**
      * @property composeMailAction
      * @type Tine.widgets.ActionUpdater
      * @private
      */
-    composeMailAction: null,
+    composeMailActionBCC: null,
     
     /**
      * @property composeMailBtn
@@ -170,7 +227,7 @@ Ext.apply(Tine.Felamimail.GridPanelHook.prototype, {
      * 
      * @param {Button} btn 
      */
-    onComposeEmail: function(btn) {
+    onComposeEmail: function(btn,to) {
         if (this.getGridPanel().grid) {
             var sm = this.getGridPanel().grid.getSelectionModel(),
                 mailAddresses = sm.isFilterSelect ? null : this.getMailAddresses(this.getGridPanel().grid.getSelectionModel().getSelections());
@@ -179,10 +236,27 @@ Ext.apply(Tine.Felamimail.GridPanelHook.prototype, {
                 mailAddresses = this.mailAddresses;
         }
 
-        var record = new Tine.Felamimail.Model.Message({
-            subject: (this.subject) ? this.subject : '',
-            to: mailAddresses
-        }, 0);
+        if( to == "CC")
+        {
+            var record = new Tine.Felamimail.Model.Message({
+                subject: (this.subject) ? this.subject : '',
+                cc: mailAddresses
+            }, 0);
+        }
+        else if( to == "BCC")
+        {
+            var record = new Tine.Felamimail.Model.Message({
+                subject: (this.subject) ? this.subject : '',
+                bcc: mailAddresses
+            }, 0);
+        }
+        else
+        {
+            var record = new Tine.Felamimail.Model.Message({
+                subject: (this.subject) ? this.subject : '',
+                to: mailAddresses
+            }, 0);
+        }
         var popupWindow = Tine.Felamimail.MessageEditDialog.openWindow({
             selectionFilter: sm && sm.isFilterSelect ? Ext.encode(sm.getSelectionFilter()) : null,
             record: record
@@ -190,6 +264,33 @@ Ext.apply(Tine.Felamimail.GridPanelHook.prototype, {
     },
     
     /**
+     * compose an email to selected contacts
+     * 
+     * @param {Button} btn 
+     */
+    onComposeEmailTO: function(btn) {
+        this.onComposeEmail( btn, "TO" );
+    },
+
+    /**
+     * compose an email to selected contacts
+     * 
+     * @param {Button} btn 
+     */
+    onComposeEmailCC: function(btn) {
+        this.onComposeEmail( btn, "CC" );
+    },
+
+    /**
+     * compose an email to selected contacts
+     * 
+     * @param {Button} btn 
+     */
+    onComposeEmailBCC: function(btn) {
+        this.onComposeEmail( btn, "BCC" );
+    },
+
+    /**
      * add to action updater the first time we render
      */
     onRender: function() {
@@ -199,6 +300,15 @@ Ext.apply(Tine.Felamimail.GridPanelHook.prototype, {
         if (registeredActions.indexOf(this.composeMailAction) < 0) {
             actionUpdater.addActions([this.composeMailAction]);
         }
+        if (registeredActions.indexOf(this.composeMailActionTO) < 0) {
+            actionUpdater.addActions([this.composeMailActionTO]);
+        }
+        if (registeredActions.indexOf(this.composeMailActionCC) < 0) {
+            actionUpdater.addActions([this.composeMailActionCC]);
+        }
+        if (registeredActions.indexOf(this.composeMailActionBCC) < 0) {
+            actionUpdater.addActions([this.composeMailActionBCC]);
+        }
     },
     
     /**
index efa37a7..8f440cd 100644 (file)
@@ -122,4 +122,14 @@ class Filemanager_Setup_Update_Release8 extends Setup_Update_Abstract
         
         $this->setApplicationVersion('Filemanager', '8.1');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_1()
+    {
+        $this->setApplicationVersion('Filemanager', '9.0');
+    }
 }
index 03e57b8..b37cb84 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Filemanager</name>
-    <version>8.1</version>
+    <version>9.0</version>
     <order>11</order>
     <depends>
         <application>Admin</application>
index 77ca29f..be41917 100644 (file)
@@ -129,4 +129,14 @@ class HumanResources_Setup_Update_Release8 extends Setup_Update_Abstract
         $this->setTableVersion('humanresources_account', '4');
         $this->setApplicationVersion('HumanResources', '8.6');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_6()
+    {
+        $this->setApplicationVersion('HumanResources', '9.0');
+    }
 }
index 70a2a3f..2174c03 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>HumanResources</name>
-    <version>8.6</version>
+    <version>9.0</version>
     <order>51</order>
     <depends>
         <application>Calendar</application>
diff --git a/tine20/Inventory/Setup/Update/Release8.php b/tine20/Inventory/Setup/Update/Release8.php
new file mode 100644 (file)
index 0000000..4b07774
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Inventory
+ * @subpackage  Setup
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL3
+ * @copyright   Copyright (c) 2012-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Stefanie Stamer <s.stamer@metaways.de>
+ */
+class Inventory_Setup_Update_Release8 extends Setup_Update_Abstract
+{
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_0()
+    {
+        $this->setApplicationVersion('Inventory', '9.0');
+    }
+}
index 548ca4d..e632650 100644 (file)
@@ -2,7 +2,7 @@
 <application>
     <name>Inventory</name>
     <!-- gettext('Inventory') -->   
-    <version>8.0</version>
+    <version>9.0</version>
     <order>60</order>
     <status>enabled</status>
     <tables>
diff --git a/tine20/Phone/Setup/Update/Release8.php b/tine20/Phone/Setup/Update/Release8.php
new file mode 100644 (file)
index 0000000..40406e4
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Phone
+ * @subpackage  Setup
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL3
+ * @copyright   Copyright (c) 2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Stefanie Stamer <s.stamer@metaways.de>
+ */
+class Phone_Setup_Update_Release8 extends Setup_Update_Abstract
+{
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_0()
+    {
+        $this->setApplicationVersion('Phone', '9.0');
+    }
+}
index d98348d..04443f7 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Phone</name>
-    <version>8.0</version>
+    <version>9.0</version>
     <order>11</order>
     <depends>
         <application>Admin</application>
diff --git a/tine20/Projects/Setup/Update/Release8.php b/tine20/Projects/Setup/Update/Release8.php
new file mode 100644 (file)
index 0000000..4fae427
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Projects
+ * @subpackage  Setup
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL3
+ * @copyright   Copyright (c) 2012-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Stefanie Stamer <s.stamer@metaways.de>
+ */
+class Projects_Setup_Update_Release8 extends Setup_Update_Abstract
+{
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_0()
+    {
+        $this->setApplicationVersion('Projects', '9.0');
+    }
+}
index 2f02677..70a4cb7 100644 (file)
@@ -2,7 +2,7 @@
 <application>
     <name>Projects</name>
     <!-- gettext('Projects') -->   
-    <version>8.0</version>
+    <version>9.0</version>
     <order>60</order>
     <status>enabled</status>
     <tables>
index 8c5f1b2..6b472a9 100644 (file)
@@ -68,7 +68,9 @@ class Sales_Backend_Contract extends Tinebase_Backend_Sql_Abstract
         
         $date->addMonth(7);
         $date->subSecond(1);
-        $sql .= ' AND '   . $db->quoteInto($db->quoteIdentifier('start_date') . ' <= ?', $date);
+        $sql .= ' AND ' . $db->quoteInto($db->quoteIdentifier('start_date') . ' <= ?', $date)
+              . ' AND ' . $db->quoteIdentifier('start_date') . ' IS NOT NULL'
+              . ' AND ' . $db->quoteIdentifier('start_date') . ' <> "0000-00-00 00:00:00"';
     
         return array_keys($db->fetchAssoc($sql));
     }
index d06479d..a56d5d4 100644 (file)
@@ -256,6 +256,16 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
             
             $this->_currentBillingContract = NULL;
         }
+
+        if(! $contract->start_date) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
+                $failure = 'Could not create auto invoice for contract "' . $contract->number . '", because no start date is set!';
+                $this->_autoInvoiceIterationFailures[] = $failure;
+                Tinebase_Core::getLogger()->log(__METHOD__ . '::' . __LINE__ . ' ' . $failure, Zend_Log::INFO);
+            }
+
+            $this->_currentBillingContract = NULL;
+        }
         
         return ($this->_currentBillingContract != NULL);
     }
index b6c715c..1c407d2 100644 (file)
@@ -2070,4 +2070,14 @@ class Sales_Setup_Update_Release8 extends Setup_Update_Abstract
         $this->setTableVersion('sales_purchase_invoices', 2);
         $this->setApplicationVersion('Sales', '8.30');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_30()
+    {
+        $this->setApplicationVersion('Sales', '9.0');
+    }
 }
index cfa4b6a..bec4a95 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Sales</name>
-    <version>8.30</version>
+    <version>9.0</version>
     <order>50</order>
     <status>enabled</status>
     <tables>
index b72928a..2e058de 100644 (file)
@@ -32,6 +32,7 @@ Ext.namespace('Tine.Sales');
 Tine.Sales.ContractEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
     windowWidth: 800,
     windowHeight: 600,
+    displayNotes: true,
 
     /**
      * autoset
@@ -65,21 +66,10 @@ Tine.Sales.ContractEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
         this.autoGenerateNumber = (Tine.Sales.registry.get('config').contractNumberGeneration.value == 'auto') ? true : false;
         this.validateNumber = Tine.Sales.registry.get('config').contractNumberValidation.value;
 
-        this.initToolbar();
-        
         Tine.Sales.ContractEditDialog.superclass.initComponent.call(this);
     },
     
     /**
-     * initializes the toolbar
-     */
-    initToolbar: function() {
-        var addNoteButton = new Tine.widgets.activities.ActivitiesAddButton({});
-
-        Tine.Sales.ContractEditDialog.superclass.initToolbar.call(this);
-    },
-    
-    /**
      * called on multiple edit
      *
      * @return {Boolean}
@@ -348,11 +338,7 @@ Tine.Sales.ContractEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
                         })
                     ]
                 }]
-            }, this.productGridPanel, new Tine.widgets.activities.ActivitiesTabPanel({
-                app: this.appName,
-                record_id: this.record.id,
-                record_model: this.appName + '_Model_' + this.recordClass.getMeta('modelName')
-            })]
+            }]
         };
     }
 });
index c8822e4..8c6bf2c 100644 (file)
@@ -265,7 +265,8 @@ Tine.Sales.CustomerEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
                                     allowDecimals : true
                                 }),
                                 decimalPrecision: 2,
-                                decimalSeparator: Tine.Tinebase.registry.get('decimalSeparator')
+                                decimalSeparator: Tine.Tinebase.registry.get('decimalSeparator'),
+                                regex: /^[0-9]+\.?[0-9]*$/
                             }], [{
                                 name: 'iban',
                                 fieldLabel: this.app.i18n._('IBAN')
index cf1883c..21f9924 100644 (file)
@@ -346,6 +346,7 @@ Tine.Sales.InvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
             }),
             name: 'sales_tax',
             decimalSeparator: Tine.Tinebase.registry.get('decimalSeparator'),
+            regex: /^[0-9]+\.?[0-9]*$/,
             fieldLabel: this.app.i18n._('Sales Tax (percent)'),
             columnWidth: 1/3,
             listeners: {
index 45672cf..454e300 100644 (file)
@@ -30,16 +30,11 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
     /**
      * @private
      */
-    tbarItems: null,
     evalGrants: false,
     
     windowWidth: 900,
     windowHeight: 700,
-    
-    initComponent: function() {
-        this.tbarItems = [{xtype: 'widget-activitiesaddbutton'}];
-        Tine.Sales.PurchaseInvoiceEditDialog.superclass.initComponent.call(this);
-    },
+    displayNotes: true,
     
     /**
      * is form valid?
@@ -47,8 +42,7 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
      * @return {Boolean}
      */
     isValid: function() {
-        var isValid = Tine.Sales.PurchaseInvoiceEditDialog.superclass.isValid.call(this);
-        return isValid;
+        return Tine.Sales.PurchaseInvoiceEditDialog.superclass.isValid.call(this);
     },
     
     /**
@@ -172,19 +166,6 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
     },
     
     /**
-     * calculates price gross by price net and tax
-     */
-    calcGrossOld: function() {
-        var net = parseFloat(this.priceNetField.getValue());
-        var tax = parseFloat(this.salesTaxField.getValue());
-        var gross = net + (net * (tax / 100));
-        
-        var grosscent = Math.ceil(gross * 100);
-        
-        this.priceGrossField.setValue(grosscent / 100);
-    },
-    
-    /**
      * returns dialog
      * 
      * NOTE: when this method gets called, all initalisation is done.
@@ -299,6 +280,7 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
             decimalSeparator: Tine.Tinebase.registry.get('decimalSeparator'),
             fieldLabel: this.app.i18n._('Sales Tax (percent)'),
             columnWidth: 1/4,
+            regex: /^[0-9]+\.?[0-9]*$/,
             listeners: {
                 scope: this,
                 spin: this.onUpdateSalesTax.createDelegate(this),
@@ -311,20 +293,16 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
             decimalPrecision: 2,
             strategy: new Ext.ux.form.Spinner.NumberStrategy({
                 incrementValue : 1,
-                alternateIncrementValue: 0.1,
                 minValue: 0,
                 maxValue: 100,
-                allowDecimals: 2
+                allowDecimals: 0
             }),
             name: 'discount',
             decimalSeparator: Tine.Tinebase.registry.get('decimalSeparator'),
             fieldLabel: this.app.i18n._('Discount (%)'),
             columnWidth: 1/4,
-            //listeners: {
-            //    scope: this,
-            //    spin: this.calcGross.createDelegate(this),
-            //    blur: this.calcGross.createDelegate(this)
-            //}
+            value: 0,
+            regex: /^[0-9]+\.?[0-9]*$/
         });
         
         var items = [{
@@ -351,7 +329,7 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
                                 name: 'number',
                                 fieldLabel: this.app.i18n._('Invoice Number'),
                                 columnWidth: 1/4,
-                                allowBlank: false,
+                                allowBlank: false
                                 //readOnly: ! Tine.Tinebase.common.hasRight('set_invoice_number', 'Sales'),
                                 //emptyText: this.app.i18n._('automatically set...')
                             }, {
@@ -376,7 +354,7 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
                                     xtype: 'extuxclearabledatefield',
                                     name: 'overdue_at',
                                     fieldLabel: this.app.i18n._('Overdue date'),
-                                    columnWidth: 1/4,
+                                    columnWidth: 1/4
                                     //emptyText: (this.record.get('is_auto') == 1) ? this.app.i18n._('automatically set...') : ''
                             }], [
                                 this.priceNetField,
@@ -390,7 +368,7 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
                                     name: 'discount_until',
                                     //allowBlank: false,
                                     fieldLabel: this.app.i18n._('Discount until'),
-                                    columnWidth: 1/4,
+                                    columnWidth: 1/4
                                     //emptyText: (this.record.get('is_auto') == 1) ? this.app.i18n._('automatically set...') : ''
                                 }
                             ]
@@ -411,13 +389,13 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
                                     xtype: 'extuxclearabledatefield',
                                     name: 'dunned_at',
                                     fieldLabel: this.app.i18n._('Dun date'),
-                                    columnWidth: 1/4,
+                                    columnWidth: 1/4
                                 }, {
                                     xtype: 'extuxclearabledatefield',
                                     name: 'payed_at',
                                     //allowBlank: false,
                                     fieldLabel: this.app.i18n._('Payed at'),
-                                    columnWidth: 1/4,
+                                    columnWidth: 1/4
                                     //emptyText: (this.record.get('is_auto') == 1) ? this.app.i18n._('automatically set...') : ''
                                 },
                                 // is_payed
@@ -496,12 +474,6 @@ Tine.Sales.PurchaseInvoiceEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog
                             requiredGrant: 'editGrant'
                         }]
                     }),
-                    new Tine.widgets.activities.ActivitiesPanel({
-                        app: 'Sales',
-                        showAddNoteForm: false,
-                        border: false,
-                        bodyStyle: 'border:1px solid #B5B8C8;'
-                    }),
                     new Tine.widgets.tags.TagPanel({
                         app: 'Sales',
                         border: false,
index f53aef9..27e067d 100644 (file)
@@ -25,11 +25,7 @@ Ext.ns('Tine.Sales');
 Tine.Sales.SupplierEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
     windowWidth: 900,
     windowHeight: 800,
-    
-    initComponent: function() {
-        this.tbarItems = [{xtype: 'widget-activitiesaddbutton'}];
-        Tine.Sales.SupplierEditDialog.superclass.initComponent.call(this);
-    },
+    displayNotes: true,
     
     /**
      * 
@@ -245,7 +241,8 @@ Tine.Sales.SupplierEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
                                     allowDecimals : true
                                 }),
                                 decimalPrecision: 2,
-                                decimalSeparator: Tine.Tinebase.registry.get('decimalSeparator')
+                                decimalSeparator: Tine.Tinebase.registry.get('decimalSeparator'),
+                                regex: /^[0-9]+\.?[0-9]*$/
                             }], [{
                                 name: 'iban',
                                 fieldLabel: this.app.i18n._('IBAN')
@@ -379,12 +376,6 @@ Tine.Sales.SupplierEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
                             requiredGrant: 'editGrant'
                         }]
                     }),
-                    new Tine.widgets.activities.ActivitiesPanel({
-                        app: 'Sales',
-                        showAddNoteForm: false,
-                        border: false,
-                        bodyStyle: 'border:1px solid #B5B8C8;'
-                    }),
                     new Tine.widgets.tags.TagPanel({
                         app: 'Sales',
                         border: false,
index cede0c5..121c03e 100644 (file)
@@ -117,16 +117,18 @@ class Setup_Backend_Mysql extends Setup_Backend_Abstract
                 array()
             )
             ->where($this->_db->quoteIdentifier('table_constraints.CONSTRAINT_SCHEMA')    . ' = ?', $this->_config->database->dbname)
+            ->where($this->_db->quoteIdentifier('table_constraints.TABLE_SCHEMA')         . ' = ?', $this->_config->database->dbname)
+            ->where($this->_db->quoteIdentifier('key_column_usage.TABLE_SCHEMA')          . ' = ?', $this->_config->database->dbname)
             ->where($this->_db->quoteIdentifier('table_constraints.CONSTRAINT_TYPE')      . ' = ?', 'FOREIGN KEY')
             ->where($this->_db->quoteIdentifier('key_column_usage.REFERENCED_TABLE_NAME') . ' = ?', SQL_TABLE_PREFIX . $tableName);
-        
+
         $foreignKeyNames = array();
-        
+
         $stmt = $select->query();
         while ($row = $stmt->fetch()) {
             $foreignKeyNames[$row['CONSTRAINT_NAME']] = array(
-                'table_name'      => str_replace(SQL_TABLE_PREFIX, '', $row['TABLE_NAME']), 
-                'constraint_name' => str_replace(SQL_TABLE_PREFIX, '', $row['CONSTRAINT_NAME']));
+                'table_name'      => preg_replace('/' . SQL_TABLE_PREFIX . '/', '', $row['TABLE_NAME']),
+                'constraint_name' => preg_replace('/' . SQL_TABLE_PREFIX. '/', '', $row['CONSTRAINT_NAME']));
         }
         
         return $foreignKeyNames;
index c881d9f..c7cb7f5 100644 (file)
@@ -18,7 +18,7 @@
  */
 class Setup_Exception_PromptUser extends Setup_Exception
 {
-    public function __construct($_message, $_code) {
-        parent::__construct('This update could be run from cli only!', 901);
+    public function __construct($_message, $_code = 901) {
+        parent::__construct('This update could be run from cli only!', $_code);
     }
 }
index 260e6ea..8a119db 100644 (file)
@@ -318,6 +318,8 @@ class Setup_Frontend_Json extends Tinebase_Frontend_Abstract
      * @see Tinebase_Application_Json_Abstract
      * 
      * @return mixed array 'variable name' => 'data'
+     *
+     * TODO DRY: most of this already is part of Tinebase_Frontend_Json::_getAnonymousRegistryData
      */
     public function getAllRegistryData()
     {
@@ -340,6 +342,8 @@ class Setup_Frontend_Json extends Tinebase_Frontend_Abstract
                 'packageString' => TINE20SETUP_PACKAGESTRING,
                 'releaseTime'   => TINE20SETUP_RELEASETIME
             ),
+            'maxFileUploadSize' => Tinebase_Helper::convertToBytes(ini_get('upload_max_filesize')),
+            'maxPostSize'       => Tinebase_Helper::convertToBytes(ini_get('post_max_size')),
         );
         
         return $registryData;
index addb2be..599dd7b 100644 (file)
@@ -87,11 +87,12 @@ class Setup_Update_Abstract
                 ->where(    $this->_db->quoteIdentifier('name') . ' = ?', $_tableName)
                 ->orwhere(  $this->_db->quoteIdentifier('name') . ' = ?', SQL_TABLE_PREFIX . $_tableName);
 
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
+            ' ' . $select->__toString());
+
         $stmt = $select->query();
         $rows = $stmt->fetchAll();
-        
-        //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $select->__toString());
-        
+
         $result = (count($rows) > 0 && isset($rows[0]['version'])) ? $rows[0]['version'] : 0;
         
         return $result;
@@ -233,7 +234,7 @@ class Setup_Update_Abstract
                 try {
                     if ($userFound === FALSE) {
                         echo PHP_EOL;
-                        echo 'The user "' . $user . '" could not be found!' . PHP_EOL . PHP_EOL;
+                        echo 'The user could not be found!' . PHP_EOL . PHP_EOL;
                     }
                     
                     $user = Tinebase_Server_Cli::promptInput('Please enter an admin username to perform updates ');
@@ -255,7 +256,7 @@ class Setup_Update_Abstract
             } while (! $userFound);
             
         } else {
-            throw new Setup_Exception_PromptUser();
+            throw new Setup_Exception_PromptUser('no CLI call');
         }
         
         return $userAccount;
index 098fc1a..2d037c2 100644 (file)
@@ -37,4 +37,14 @@ class Tasks_Setup_Update_Release8 extends Setup_Update_Abstract
         $this->setTableVersion('tasks', 8);
         $this->setApplicationVersion('Tasks', '8.1');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_1()
+    {
+        $this->setApplicationVersion('Tasks', '9.0');
+    }
 }
index d9f8fab..3f975e1 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Tasks</name>
-    <version>8.1</version>
+    <version>9.0</version>
     <order>30</order>
     <depends>
         <application>Admin</application>
index b0c3729..d0ae22f 100644 (file)
@@ -72,4 +72,14 @@ class Timetracker_Setup_Update_Release8 extends Setup_Update_Abstract
         
         $this->setApplicationVersion('Timetracker', '8.3');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_3()
+    {
+        $this->setApplicationVersion('Timetracker', '9.0');
+    }
 }
index 5b7500d..e26cfb3 100644 (file)
@@ -2,7 +2,7 @@
 <application>
     <name>Timetracker</name>
     <!-- gettext('Timetracker') -->   
-    <version>8.3</version>
+    <version>9.0</version>
     <order>60</order>
     <status>enabled</status>
     <depends>
index 6bdbb00..bae6e16 100644 (file)
@@ -77,10 +77,12 @@ Tine.Timetracker.TimeaccountEditDialog = Ext.extend(Tine.widgets.dialog.EditDial
             fieldLabel: this.app.i18n._('Unit Price'),
             name: 'price',
             allowNegative: false
-            //decimalSeparator: ','
         }, {
+            xtype: 'numberfield',
             fieldLabel: this.app.i18n._('Budget'),
-            name: 'budget'
+            name: 'budget',
+            allowNegative: false,
+            value: 0
         }, {
             fieldLabel: this.app.i18n._('Status'),
             name: 'is_open',
index da29026..668ebae 100755 (executable)
@@ -80,7 +80,7 @@ class Tinebase_Auth
     const FAILURE_PASSWORD_EXPIRED      = -101;
     
     /**
-     * Failure due the account is temporarly blocked
+     * Failure due the account is temporarily blocked
      */
     const FAILURE_BLOCKED               = -102;
         
index 26a329a..8bd0b1e 100644 (file)
@@ -73,10 +73,13 @@ class Tinebase_Controller extends Tinebase_Controller_Event
      * @param   string                           $password
      * @param   Zend_Controller_Request_Abstract $request
      * @param   string                           $clientIdString
-     * @param   string                           $securitycode   the security code(captcha)
+     *
      * @return  bool
+     *
+     * TODO what happened to the $securitycode parameter?
+     *  ->  @param   string                           $securitycode   the security code(captcha)
      */
-    public function login($loginName, $password, \Zend\Http\Request $request, $clientIdString = NULL, $securitycode = NULL)
+    public function login($loginName, $password, \Zend\Http\Request $request, $clientIdString = NULL)
     {
         $authResult = Tinebase_Auth::getInstance()->authenticate($loginName, $password);
         
@@ -91,7 +94,7 @@ class Tinebase_Controller extends Tinebase_Controller_Event
         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
             __METHOD__ . '::' . __LINE__ . " Login with username {$accessLog->login_name} from {$accessLog->ip} succeeded.");
         
-        $this->_setSessionId($user, $accessLog, $clientIdString);
+        $this->_setSessionId($accessLog);
         
         $this->initUser($user);
         
@@ -158,21 +161,31 @@ class Tinebase_Controller extends Tinebase_Controller_Event
         if ($_accessLog->result == Tinebase_Auth::SUCCESS && $_user->accountStatus !== Tinebase_User::STATUS_ENABLED) {
             // is the account enabled?
             if ($_user->accountStatus == Tinebase_User::STATUS_DISABLED) {
-                if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Account: '. $_user->accountLoginName . ' is disabled');
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::'
+                    . __LINE__ . ' Account: '. $_user->accountLoginName . ' is disabled');
                 $_accessLog->result = Tinebase_Auth::FAILURE_DISABLED;
             }
             
             // is the account expired?
             else if ($_user->accountStatus == Tinebase_User::STATUS_EXPIRED) {
-                if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Account: '. $_user->accountLoginName . ' password is expired');
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::'
+                    . __LINE__ . ' Account: '. $_user->accountLoginName . ' password is expired');
                 $_accessLog->result = Tinebase_Auth::FAILURE_PASSWORD_EXPIRED;
             }
             
             // too many login failures?
             else if ($_user->accountStatus == Tinebase_User::STATUS_BLOCKED) {
-                if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Account: '. $_user->accountLoginName . ' is blocked');
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::'
+                    . __LINE__ . ' Account: '. $_user->accountLoginName . ' is blocked');
                 $_accessLog->result = Tinebase_Auth::FAILURE_BLOCKED;
             }
+
+            // Tinebase run permission
+            else if (! $_user->hasRight('Tinebase', Tinebase_Acl_Rights_Abstract::RUN)) {
+                if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::'
+                    . __LINE__ . ' Account: '. $_user->accountLoginName . ' has not permissions for Tinebase');
+                $_accessLog->result = Tinebase_Auth::FAILURE_DISABLED;
+            }
         }
     }
     
@@ -604,10 +617,9 @@ class Tinebase_Controller extends Tinebase_Controller_Event
     /**
      * set session for current request
      * 
-     * @param Tinebase_Model_FullUser $user
      * @param Tinebase_Model_AccessLog $accessLog
      */
-    protected function _setSessionId(Tinebase_Model_FullUser $user, Tinebase_Model_AccessLog &$accessLog)
+    protected function _setSessionId(Tinebase_Model_AccessLog &$accessLog)
     {
         if (in_array($accessLog->clienttype, array(Tinebase_Server_WebDAV::REQUEST_TYPE, ActiveSync_Server_Http::REQUEST_TYPE))) {
             try {
index f31a95f..a98a248 100644 (file)
@@ -231,33 +231,38 @@ class Tinebase_Export_Spreadsheet_Ods extends Tinebase_Export_Spreadsheet_Abstra
             }
             $this->_addColumnStyle('co0', $defaultStyles);
         }
-        
-        foreach($this->_config->columns->column as $column) {
 
-            if ($column->style) {
-                if (! $defaultStyles) {
-                    $msg = 'If a column contains style, the "defaultColumnStyle" has to be defined!';
-                    
-                    if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
-                        Tinebase_Core::getLogger()->log(__METHOD__ . '::' . __LINE__ . ' ' . $msg . ' Definition Name: ' . (string) $this->_config->name);
+        if (isset($this->_config->columns)) {
+            foreach ($this->_config->columns->column as $column) {
+
+                if ($column->style) {
+                    if (!$defaultStyles) {
+                        $msg = 'If a column contains style, the "defaultColumnStyle" has to be defined!';
+
+                        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
+                            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $msg . ' Definition Name: ' . (string)$this->_config->name);
+                        }
+
+                        throw new Tinebase_Exception_UnexpectedValue($msg);
                     }
-                    
-                    throw new Tinebase_Exception_UnexpectedValue($msg);
-                }
-                
-                $columnStyles = array();
-                foreach($column->style as $name => $style) {
-                    $columnStyles[$name] = (string) $style;
+
+                    $columnStyles = array();
+                    foreach ($column->style as $name => $style) {
+                        $columnStyles[$name] = (string)$style;
+                    }
+
+                    $this->_addColumnStyle($classPrefix . $index, $columnStyles);
+
+                    $this->_columnStyles[$index] = $classPrefix . $index;
+                } else {
+                    $this->_columnStyles[$index] = 'co0';
                 }
-                
-                $this->_addColumnStyle($classPrefix . $index, $columnStyles);
-                
-                $this->_columnStyles[$index] = $classPrefix . $index;
-            } else {
-                $this->_columnStyles[$index] = 'co0';
+
+                $index++;
             }
-            
-            $index++;
+        } else {
+            if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
+                ' No column config found');
         }
         
         foreach($this->_columnStyles as $key => $style) {
index 5e5519b..74df7cd 100644 (file)
@@ -625,6 +625,11 @@ class Tinebase_Group_Ldap extends Tinebase_Group_Sql implements Tinebase_Group_I
         
         $this->getLdap()->update($dn, $ldapData);
         
+        if ($metaData['cn'] != $ldapData['cn']) {
+            $newDn = "cn={$ldapData['cn']},{$this->_options['groupsDn']}";
+            $this->_ldap->rename($dn, $newDn);
+        }
+        
         $group = $this->getGroupByIdFromSyncBackend($_group);
 
         return $group;
index 74c87dc..58001d0 100644 (file)
@@ -40,7 +40,7 @@ class Tinebase_Helper
         
         foreach ((array)$values as $id => $value) {
             if (is_array($value)) {
-                $value = $this->arrayHash($value, $includeKeys);
+                $value = self::arrayHash($value, $includeKeys);
             }
             
             hash_update($ctx, ($includeKeys ? $id : null) . (string)$value);
index 9547e9b..8d4ab75 100644 (file)
@@ -445,7 +445,7 @@ class Tinebase_Relations
     }
     
     /**
-     * resolved app records and filles the related_record property with the corresponding record
+     * resolved app records and fills the related_record property with the corresponding record
      * 
      * NOTE: With this, READ ACL is implicitly checked as non readable records won't get retuned!
      * 
@@ -457,7 +457,7 @@ class Tinebase_Relations
      */
     protected function resolveAppRecords($_relations, $_ignoreACL = FALSE)
     {
-        // seperate relations by model
+        // separate relations by model
         $modelMap = array();
         foreach ($_relations as $relation) {
             if (!(isset($modelMap[$relation->related_model]) || array_key_exists($relation->related_model, $modelMap))) {
@@ -489,12 +489,12 @@ class Tinebase_Relations
                 }
             }
             
-            
             $getMultipleMethod = 'getMultiple';
             
             if ($modelName === 'Tinebase_Model_User') {
                 // @todo add related backend here
                 //$appController = Tinebase_User::factory($relations->related_backend);
+
                 $appController = Tinebase_User::factory(Tinebase_User::getConfiguredBackend());
                 $records = $appController->$getMultipleMethod($relations->related_id);
             } else {
@@ -517,7 +517,10 @@ class Tinebase_Relations
                     continue;
                 }
             }
-            
+
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+                " Resolving " . count($relations) . " relations");
+
             foreach ($relations as $relation) {
                 $recordIndex    = $records->getIndexById($relation->related_id);
                 $relationIndex  = $_relations->getIndexById($relation->getId());
index 1909add..7912733 100644 (file)
@@ -524,4 +524,14 @@ class Tinebase_Setup_Update_Release8 extends Setup_Update_Abstract
         
         $this->setApplicationVersion('Tinebase', '8.11');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_11()
+    {
+        $this->setApplicationVersion('Tinebase', '9.0');
+    }
 }
index d319b8a..051e370 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Tinebase</name>
-    <version>8.11</version>
+    <version>9.0</version>
     <tables>
         <table>
             <name>applications</name>
index 3dc6bf1..a4cdb01 100644 (file)
@@ -347,7 +347,6 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
                         
                         return $result;
                     }
-                    
                     $result = array();
                     
                     $user = Tinebase_User::getInstance()->getUserByPropertyFromSqlBackend('contactId', $contactId);
@@ -390,9 +389,8 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
                             }
                         }
                     }
-                    
                     Tinebase_Cache_PerRequest::getInstance()->save(__CLASS__, __FUNCTION__, $classCacheId, $result);
-                    $cache->save($result, $cacheId, array(), 60);
+                    $cache->save($result, $cacheId, array(), 60 * 3);
                 }
                 
                 break;
@@ -524,8 +522,8 @@ class Tinebase_WebDav_PrincipalBackend implements \Sabre\DAVACL\PrincipalBackend
                     )));
                 }
                 
-                #if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(
-                #    __METHOD__ . '::' . __LINE__ . ' path: ' . $prefixPath . ' properties: ' . print_r($filter->toArray(), true));
+                if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
+                    ' path: ' . $prefixPath . ' properties: ' . print_r($filter->toArray(), true));
                 
                 $result = Addressbook_Controller_Contact::getInstance()->search($filter, null, false, true);
                 
index ac6d347..e65bab9 100644 (file)
@@ -60,12 +60,6 @@ Tine.Tinebase.ExceptionHandler = function() {
             return true;
         }
         
-        // really bad thing: fix exists only in close source version
-        // http://www.extjs.com/forum/showthread.php?t=76860
-        if (traceHtml.match(/swf\.setDataProvider/)) {
-            return true;
-        }
-        
         // let exception bubble to browser
         return false;
     };
index 14e4519..52dee67 100644 (file)
@@ -39,11 +39,6 @@ Ext.BLANK_IMAGE_URL = "library/ExtJS/resources/images/default/s.gif";
 Ext.SSL_SECURE_URL = "library/ExtJS/resources/images/default/s.gif";
 
 /**
- * don't fill yahoo stats
- */
-Ext.chart.Chart.CHART_URL = 'library/ExtJS/resources/charts.swf';
-
-/**
  * use native json implementation because we had problems with utf8 linebreaks (\u2028 for example)
  * @see 0003356: Special characters in telephone numbers makes addressbook stop responding
  * @see 0009416: IE9: js error in (new) lead edit dialog
index 4bdbab3..4260a36 100644 (file)
@@ -223,44 +223,31 @@ Ext.extend(Ext.ux.file.Upload, Ext.util.Observable, {
         ) && this.file) {
      
             // free browse plugin
-//            this.getInput();
+            this.getInput();
             
             if (this.isHtml5ChunkedUpload()) {
-
-//                if(this.fileSize > this.maxAllowedFileSize) { // admin confgured max file size (nothing technically)
-//                    this.createFileRecord(true);
-//                    this.fileRecord.beginEdit();
-//                    this.fileRecord.set('status', 'failure');
-//                    this.fileRecord.endEdit();
-//                    this.fireEvent('uploadfailure', this, this.fileRecord);
-//                    return this.fileRecord;
-//                }
-                
-                // calculate optimal maxChunkSize       
+                // calculate optimal maxChunkSize
                 // TODO: own method for chunked upload
                 
                 var chunkMax = this.maxChunkSize;
                 var chunkMin = this.minChunkSize;
                 var actualChunkSize = this.maxChunkSize;
 
-                if(this.fileSize > 5 * chunkMax) {
+                if (this.fileSize > 5 * chunkMax) {
                     actualChunkSize = chunkMax;
-                }
-                else {
+                } else {
                     actualChunkSize = Math.max(chunkMin, this.fileSize / 5);
                 }       
                 this.maxChunkSize = actualChunkSize;
                 
-                if(Tine.Tinebase.uploadManager && Tine.Tinebase.uploadManager.isBusy()) {
+                if (Tine.Tinebase.uploadManager && Tine.Tinebase.uploadManager.isBusy()) {
                     this.createFileRecord(true);
                     this.setQueued(true);
-                }
-                else {
+                } else {
                     this.createFileRecord(false);
                     this.fireEvent('uploadstart', this);
                     this.fireEvent('update', 'uploadstart', this, this.fileRecord);
                     this.html5ChunkedUpload();
-                    
                 }
                 
                 return this.fileRecord;
index 540b1af..685e074 100644 (file)
@@ -83,4 +83,14 @@ class Voipmanager_Setup_Update_Release8 extends Setup_Update_Abstract
         $this->setTableVersion('asterisk_sip_peers', '3');
         $this->setApplicationVersion('Voipmanager', '8.2');
     }
+    
+    /**
+     * update to 9.0
+     *
+     * @return void
+     */
+    public function update_2()
+    {
+        $this->setApplicationVersion('Voipmanager', '9.0');
+    }
 }
index a2ac863..906dfee 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Voipmanager</name>
-    <version>8.2</version>
+    <version>9.0</version>
     <order>50</order>
     <depends>
         <application>Admin</application>
index 56e8d39..641e308 100644 (file)
@@ -20,9 +20,15 @@ error_reporting(E_COMPILE_ERROR | E_CORE_ERROR | E_ERROR | E_PARSE);
 ini_set('display_errors', 1);
 ini_set('log_errors', 1);
 
-// set default internal encoding
-if (extension_loaded('iconv')) {
-    iconv_set_encoding("internal_encoding", "UTF-8");
+// iconv_set_encoding throws a deprecated exception since 5.6.*
+// Zend 1 still uses that, but at least we can effort to fix that.
+if (PHP_VERSION_ID > 50600) {
+    ini_set('default_charset', 'UTF-8');
+} else {
+    // set default internal encoding
+    if (extension_loaded('iconv')) {
+        iconv_set_encoding('internal_encoding', "UTF-8");
+    }
 }
 
 // intialize composers autoloader