0011620: add "path" filter for records
authorPhilipp Schüle <p.schuele@metaways.de>
Tue, 16 Feb 2016 16:35:30 +0000 (17:35 +0100)
committerPhilipp Schüle <p.schuele@metaways.de>
Fri, 19 Feb 2016 21:54:56 +0000 (22:54 +0100)
* generic path creation for records (with parent/child relations)
* path creation for contacts (with list memberships and roles)
* path rebuild for current record is done in action queue
* trigger path updates for related records
* make path filter work for parent contacts/group/roles

https://forge.tine20.org/view.php?id=11620

Change-Id: If2185fed74785443f77c3948d7350e18d4a4fe89
Reviewed-on: http://gerrit.tine20.com/customers/2734
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
Tested-by: Philipp Schüle <p.schuele@metaways.de>
25 files changed:
tests/tine20/Tinebase/AllTests.php
tests/tine20/Tinebase/Record/AllTests.php [new file with mode: 0644]
tests/tine20/Tinebase/Record/AutoRecord.php [deleted file]
tests/tine20/Tinebase/Record/ContainerTest.php [deleted file]
tests/tine20/Tinebase/Record/PathTest.php [new file with mode: 0644]
tests/tine20/Tinebase/Record/PersistentObserverTest.php
tine20/Addressbook/Controller/Contact.php
tine20/Addressbook/Controller/List.php
tine20/Addressbook/Convert/Contact/Json.php
tine20/Addressbook/Model/Contact.php
tine20/Addressbook/Model/ContactFilter.php
tine20/Addressbook/js/SearchCombo.js
tine20/Tinebase/ActionQueue.php
tine20/Tinebase/Controller/Record/Abstract.php
tine20/Tinebase/Model/Filter/Path.php [new file with mode: 0644]
tine20/Tinebase/Model/Path.php [new file with mode: 0644]
tine20/Tinebase/Model/PathFilter.php [new file with mode: 0644]
tine20/Tinebase/Path/Backend/Sql.php [new file with mode: 0644]
tine20/Tinebase/Record/Abstract.php
tine20/Tinebase/Record/Path.php [new file with mode: 0644]
tine20/Tinebase/Record/RecordSet.php
tine20/Tinebase/Relations.php
tine20/Tinebase/Setup/Update/Release8.php
tine20/Tinebase/Setup/Update/Release9.php
tine20/Tinebase/Setup/setup.xml

index 7986d37..a409349 100644 (file)
@@ -40,8 +40,6 @@ class Tinebase_AllTests
         $suite->addTestSuite('Tinebase_ModelConfigurationTest');
         $suite->addTestSuite('Tinebase_DateTimeTest');
         $suite->addTestSuite('Tinebase_ExceptionTest');
-        $suite->addTestSuite('Tinebase_Record_RecordTest');
-        $suite->addTestSuite('Tinebase_Record_RecordSetTest');
         $suite->addTestSuite('Tinebase_AuthTest');
         $suite->addTestSuite('Tinebase_UserTest');
         $suite->addTestSuite('Tinebase_GroupTest');
@@ -81,6 +79,7 @@ class Tinebase_AllTests
         $suite->addTest(Tinebase_Frontend_AllTests::suite());
         $suite->addTest(Tinebase_Acl_AllTests::suite());
         $suite->addTest(Tinebase_Tree_AllTests::suite());
+        $suite->addTest(Tinebase_Record_AllTests::suite());
         $suite->addTest(Tinebase_Scheduler_AllTests::suite());
         $suite->addTest(Tinebase_WebDav_AllTests::suite());
         $suite->addTest(OpenDocument_AllTests::suite());
diff --git a/tests/tine20/Tinebase/Record/AllTests.php b/tests/tine20/Tinebase/Record/AllTests.php
new file mode 100644 (file)
index 0000000..dee52d8
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ * 
+ * @package     Tinebase
+ * @license     http://www.gnu.org/licenses/agpl.html
+ * @copyright   Copyright (c) 2016 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ */
+
+/**
+ * Test helper
+ */
+require_once dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
+
+class Tinebase_Record_AllTests
+{
+    public static function main ()
+    {
+        PHPUnit_TextUI_TestRunner::run(self::suite());
+    }
+    
+    public static function suite ()
+    {
+        $suite = new PHPUnit_Framework_TestSuite('Tine 2.0 All Record Tests');
+
+        $suite->addTestSuite('Tinebase_Record_RecordTest');
+        $suite->addTestSuite('Tinebase_Record_RecordSetTest');
+        $suite->addTestSuite('Tinebase_Record_PathTest');
+
+        return $suite;
+    }
+}
diff --git a/tests/tine20/Tinebase/Record/AutoRecord.php b/tests/tine20/Tinebase/Record/AutoRecord.php
deleted file mode 100644 (file)
index 857a4bf..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-/**
- * class for testing auto model creation
- *
- * @package     Tinebase
- * @subpackage  Record
- * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
- * @author      Alexander Stintzing <a.stintzing@metaways.de>
- * @copyright   Copyright (c) 2012 Metaways Infosystems GmbH (http://www.metaways.de)
- *
- */
-
-/**
- * class to hold Test data
- * @package Test
- */
-class Tinebase_Record_AutoRecord extends Tinebase_Record_Abstract
-{
-    /**
-     * application the record belongs to
-     * @var string
-     */
-    protected $_application = 'Addressbook';
-
-    /**
-     * array with meta information about the model (like models.js)
-     * @var array
-     */
-    protected static $_meta = array(
-        'idProperty'        => 'id',
-        'titleProperty'     => 'text',
-        'recordName'        => 'Record',
-        'recordsName'       => 'Records',
-        'containerProperty' => NULL,
-        'containerName'     => 'Containers',
-        'containersName'    => 'Containers',
-        'defaultFilter'     => 'text',
-        'hasRelations'       => true,
-        'hasCustomFields'   => true,
-        'hasNotes'          => true,
-        'hasTags'           => true,
-        'modlogActive'      => true,
-    );
-
-    /**
-     * fields for auto bootstrapping
-     * @var array
-     */
-    protected static $_fields = array(
-        'id' => array(
-            'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => false),
-            'label' => null,
-        ),
-        'text' => array(
-            'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => false),
-            'label' => 'Text',
-        ),
-        'date' => array(
-            'type' => 'date', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => false),
-            'label' => 'Date', // _('Start Date')
-        ),
-    );
-}
\ No newline at end of file
diff --git a/tests/tine20/Tinebase/Record/ContainerTest.php b/tests/tine20/Tinebase/Record/ContainerTest.php
deleted file mode 100644 (file)
index 677f254..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-/**
- * Tine 2.0 - http://www.tine20.org
- * 
- * @package     Tinebase
- * @subpackage  Record
- * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de)
- * @author      Matthias Greiling <m.greiling@metaways.de>
- */
-
-/**
- * Test helper
- */
-require_once dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . 'TestHelper.php';
-
-if (!defined('PHPUnit_MAIN_METHOD')) {
-    define('PHPUnit_MAIN_METHOD', 'Tinebase_Record_ContainerTest::main');
-}
-
-/**
- * Test class for Tinebase_Record_Container.
- * Generated by PHPUnit on 2008-02-14 at 12:25:04.
- */
-class Tinebase_Record_ContainerTest extends Tinebase_Record_AbstractTest
-{
-    /**
-     * @var    Tinebase_Record_Container
-     * @access protected
-     */
-    protected $objects;
-
-    /**
-     * Sets up the fixture, for example, opens a network connection.
-     * This method is called before a test is executed.
-     *
-     * @access protected
-     */
-    public function setUp()
-    {
-        
-       $this->objects['TestRecord'] = new Tinebase_Record_Container(array(), true);
-
-         $this->objects['TestRecord']->setFromArray(array(
-            'container_id'      => 200,
-                'container_name'    => 'test',
-                'container_type'    => 'shared',
-            'container_backend' => 1,
-                'application_id'    => 20,
-               'account_grants'    => 31,
-            )
-        , true);
-        
-        $this->expectFailure['TestRecord']['testSetId'][] = array('2','3');
-        $this->expectSuccess['TestRecord']['testSetId'][] = array('2','2');
-        
-        
-        }
-
-    /**
-     * Tears down the fixture, for example, closes a network connection.
-     * This method is called after a test is executed.
-     *
-     * @access protected
-     */
-    protected function tearDown()
-    {
-    }
-}
-
-// Call Tinebase_Record_ContainerTest::main() if this source file is executed directly.
-if (PHPUnit_MAIN_METHOD == 'Tinebase_Record_AbstractRecordTest::main') {
-    Tinebase_Record_AbstractRecordTest::main();
-}
-?>
diff --git a/tests/tine20/Tinebase/Record/PathTest.php b/tests/tine20/Tinebase/Record/PathTest.php
new file mode 100644 (file)
index 0000000..55d4326
--- /dev/null
@@ -0,0 +1,248 @@
+<?php
+/**
+ * Tine 2.0 - http://www.tine20.org
+ * 
+ * @package     Tinebase
+ * @subpackage  Record
+ * @license     http://www.gnu.org/licenses/agpl.html
+ * @copyright   Copyright (c) 2016 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ */
+
+/**
+ * Record path test class
+ */
+class Tinebase_Record_PathTest extends TestCase
+{
+    protected function setUp()
+    {
+        $this->_uit = Tinebase_Record_Path::getInstance();
+        
+        parent::setUp();
+    }
+
+    /**
+     * testBuildRelationPathForRecord
+     */
+    public function testBuildRelationPathForRecord()
+    {
+        $contact = $this->_createFatherMotherChild();
+        $result = $this->_uit->generatePathForRecord($contact);
+        $this->assertTrue($result instanceof Tinebase_Record_RecordSet);
+        $this->assertEquals(2, count($result), 'should find 2 paths for record. paths:' . print_r($result->toArray(), true));
+
+        // check both paths
+        $expectedPaths = array('/grandparent/father/tester', '/mother/tester');
+        foreach ($expectedPaths as $expectedPath) {
+            $this->assertTrue(in_array($expectedPath, $result->path), 'could not find path ' . $expectedPath . ' in '
+                . print_r($result->toArray(), true));
+        }
+
+        $result = $this->_uit->generatePathForRecord($this->_fatherRecord);
+        $this->assertEquals(1, count($result), 'should find 1 path for record. paths:' . print_r($result->toArray(), true));
+        $this->assertEquals('/grandparent/father', $result->getFirstRecord()->path);
+    }
+
+    protected function _createFatherMotherChild()
+    {
+        // create some parent / child relations for record
+        $this->_fatherRecord = $this->_getFatherWithGrandfather();
+        $motherRecord = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array(
+            'n_family' => 'mother',
+        )));
+        $relation1 = $this->_getParentRelationArray($this->_fatherRecord);
+        $relation2 = $this->_getParentRelationArray($motherRecord);
+        $contact = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array(
+            'n_family' => 'tester',
+            'relations' => array($relation1, $relation2)
+        )));
+
+        return $contact;
+    }
+
+    /**
+     * @return Tinebase_Record_Interface
+     */
+    protected function _getFatherWithGrandfather()
+    {
+        $grandParentRecord = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array(
+            'n_family' => 'grandparent'
+        )));
+        $relation = $this->_getParentRelationArray($grandParentRecord);
+        $this->_fatherRecord = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array(
+            'n_family' => 'father',
+            'relations' => array($relation)
+        )));
+
+        return $this->_fatherRecord;
+    }
+
+    /**
+     * @param $record
+     * @return array
+     */
+    protected function _getParentRelationArray($record)
+    {
+        return array(
+            'own_model'              => 'Addressbook_Model_Contact',
+            'own_backend'            => 'Sql',
+            'own_id'                 => 0,
+            'related_degree'         => Tinebase_Model_Relation::DEGREE_PARENT,
+            'type'                   => '',
+            'related_backend'        => 'Sql',
+            'related_id'             => $record->getId(),
+            'related_model'          => 'Addressbook_Model_Contact',
+            'remark'                 => NULL,
+        );
+    }
+
+    /**
+     * testBuildGroupMemberPathForContact
+     */
+    public function testBuildGroupMemberPathForContact()
+    {
+        $contact = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array(
+            'n_family' => 'tester',
+            'email'    => 'somemail@example.ru',
+        )));
+        $adbJson = new Addressbook_Frontend_Json();
+        $listRole = $adbJson->saveListRole(array(
+            'name'          => 'my role',
+            'description'   => 'my test description'
+        ));
+        $memberroles = array(array(
+            'contact_id'   => $contact->getId(),
+            'list_role_id' => $listRole['id'],
+        ));
+        $adbJson->saveList(array(
+            'name'                  => 'my test group',
+            'description'           => '',
+            'members'               => array($contact->getId()),
+            'memberroles'           => $memberroles,
+            'type'                  => Addressbook_Model_List::LISTTYPE_LIST,
+            'relations'             => array($this->_getParentRelationArray($this->_getFatherWithGrandfather()))
+        ));
+
+        $result = $this->_uit->generatePathForRecord($contact);
+        $this->assertTrue($result instanceof Tinebase_Record_RecordSet);
+        $this->assertEquals(1, count($result), 'should find 1 path for record. paths:' . print_r($result->toArray(), true));
+        $this->assertEquals('/grandparent/father/my test group/my role/tester', $result->getFirstRecord()->path);
+
+        return $contact;
+    }
+
+    /**
+     * testRebuildPathForRecords
+     */
+    public function testTriggerRebuildPathForRecords()
+    {
+        $this->_fatherRecord = $this->_getFatherWithGrandfather();
+        $relation1 = $this->_getParentRelationArray($this->_fatherRecord);
+        $contact = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array(
+            'n_family' => 'tester',
+            'relations' => array($relation1)
+        )));
+
+        $recordPaths = $this->_uit->getPathsForRecords($contact);
+        $this->assertEquals(1, count($recordPaths));
+
+        $motherRecord = Addressbook_Controller_Contact::getInstance()->create(new Addressbook_Model_Contact(array(
+            'n_family' => 'mother',
+        )));
+        $relations = $contact->relations->toArray();
+        $relation2 = $this->_getParentRelationArray($motherRecord);
+        $relation2['own_id'] = $contact->getId();
+        $relations[] = $relation2;
+        $contact->relations = $relations;
+        Addressbook_Controller_Contact::getInstance()->update($contact);
+
+        $recordPaths = $this->_uit->getPathsForRecords($contact);
+        $this->assertEquals(2, count($recordPaths));
+
+        // check both paths
+        $expectedPaths = array('/grandparent/father/tester', '/mother/tester');
+        foreach ($expectedPaths as $expectedPath) {
+            $this->assertTrue(in_array($expectedPath, $recordPaths->path), 'could not find path ' . $expectedPath . ' in '
+                . print_r($recordPaths->toArray(), true));
+        }
+
+        return $contact;
+    }
+
+    /**
+     * TODO make this work
+     */
+    public function testTriggerRebuildIfFatherChanged()
+    {
+        $this->markTestSkipped('FIXME: this should work');
+
+        $contact = $this->testTriggerRebuildPathForRecords();
+
+        // change contact name and check path in related records
+        $this->_fatherRecord->n_family = 'stepfather';
+        Addressbook_Controller_Contact::getInstance()->update($this->_fatherRecord);
+
+        $recordPaths = $this->_uit->getPathsForRecords($contact);
+        $this->assertEquals(2, count($recordPaths));
+
+        // check both paths again
+        $expectedPaths = array('/grandparent/stepfather/tester', '/mother/tester');
+        foreach ($expectedPaths as $expectedPath) {
+            $this->assertTrue(in_array($expectedPath, $recordPaths->path), 'could not find path ' . $expectedPath . ' in '
+                . print_r($recordPaths->toArray(), true));
+        }
+    }
+
+    /**
+     * testPathFilter
+     */
+    public function testPathFilter()
+    {
+        $this->testBuildGroupMemberPathForContact();
+
+        $filterValues = array(
+            'father' => 2,
+            'grandparent' => 3,
+            'my test group' => 1,
+            'my role' => 1,
+            'somemail@example.ru' => 1
+        );
+        foreach ($filterValues as $value => $expectedCount) {
+
+            $filter = new Addressbook_Model_ContactFilter($this->_getPathFilterArray($value));
+            $result = Addressbook_Controller_Contact::getInstance()->search($filter);
+            $this->assertEquals($expectedCount, count($result),
+                'search string: ' . $value . ' / result: ' .
+                    print_r($result->toArray(), true));
+        }
+    }
+
+    protected function _getPathFilterArray($value)
+    {
+        return array(
+            array(
+                'condition' => 'OR',
+                'filters' => array(
+                    array('field' => 'query', 'operator' => 'contains', 'value' => $value),
+                    array('field' => 'path', 'operator' => 'contains', 'value' => $value)
+                )
+            )
+        );
+    }
+
+    public function testPathResolvingForContacts()
+    {
+        $this->testBuildGroupMemberPathForContact();
+
+        $adbJson = new Addressbook_Frontend_Json();
+        $filter = $this->_getPathFilterArray('father');
+
+        $result = $adbJson->searchContacts($filter, array());
+
+        $this->assertEquals(2, $result['totalcount']);
+        $firstRecord = $result['results'][0];
+        $this->assertTrue(isset($firstRecord['paths']), 'paths should be set in record' . print_r($firstRecord, true));
+        $this->assertEquals(1, count($firstRecord['paths']));
+        $this->assertContains('/grandparent', $firstRecord['paths'][0]['path'], 'could not find grandparent in paths of record' . print_r($firstRecord, true));
+    }
+}
index 9f7a38f..aa7589d 100644 (file)
@@ -7,6 +7,8 @@
  * @license     http://www.gnu.org/licenses/agpl.html
  * @copyright   Copyright (c) 2007-2008 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Cornelius Weiss <c.weiss@metaways.de>
+ *
+ * @deprecated  remove me
  */
 
 /**
index 1c0b3a8..a799250 100644 (file)
@@ -6,7 +6,7 @@
  * @subpackage  Controller
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Lars Kneschke <l.kneschke@metaways.de>
- * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2007-2016 Metaways Infosystems GmbH (http://www.metaways.de)
  * 
  */
 
@@ -24,13 +24,14 @@ class Addressbook_Controller_Contact extends Tinebase_Controller_Record_Abstract
      * @var boolean
      */
     protected $_setGeoDataForContacts = FALSE;
-    
+
     /**
      * the constructor
      *
      * don't use the constructor. use the singleton 
      */
-    private function __construct() {
+    private function __construct()
+    {
         $this->_applicationName = 'Addressbook';
         $this->_modelName = 'Addressbook_Model_Contact';
         $this->_backend = Addressbook_Backend_Factory::factory(Addressbook_Backend_Factory::SQL);
@@ -41,6 +42,7 @@ class Addressbook_Controller_Contact extends Tinebase_Controller_Record_Abstract
             array('n_given', 'n_family', 'org_name'),
             array('email'),
         ));
+        $this->_useRecordPaths = true;
         
         // fields used for private and company address
         $this->_addressFields = array('locality', 'postalcode', 'street', 'countryname');
@@ -261,7 +263,7 @@ class Addressbook_Controller_Contact extends Tinebase_Controller_Record_Abstract
     
     /**
      * inspect update of one record (after update)
-     * 
+     *
      * @param   Tinebase_Record_Interface $updatedRecord   the just updated record
      * @param   Tinebase_Record_Interface $record          the update record
      * @param   Tinebase_Record_Interface $currentRecord   the current record (before update)
@@ -273,7 +275,7 @@ class Addressbook_Controller_Contact extends Tinebase_Controller_Record_Abstract
             Tinebase_User::getInstance()->updateContact($updatedRecord);
         }
     }
-    
+
     /**
      * delete one record
      * - don't delete if it belongs to an user account
@@ -551,4 +553,55 @@ class Addressbook_Controller_Contact extends Tinebase_Controller_Record_Abstract
                     
         return $result;
     }
+
+    /**
+     * generates path for the contact
+     *
+     * - we add to the path:
+     *      - lists contact is member of
+     *      - we add list role memberships
+     *
+     * @param Tinebase_Record_Abstract $record
+     * @return Tinebase_Record_RecordSet
+     */
+    public function generatePathForRecord($record)
+    {
+        $result = new Tinebase_Record_RecordSet('Tinebase_Model_Path');
+
+        // fetch all groups and role memberships and add to path
+        $listIds = Addressbook_Controller_List::getInstance()->getMemberships($record);
+        foreach ($listIds as $listId) {
+            $list = Addressbook_Controller_List::getInstance()->get($listId);
+            $listPaths = $this->_getPathsOfRecord($list);
+            if (count($listPaths) === 0) {
+                // add self
+                $listPaths->addRecord(new Tinebase_Model_Path(array(
+                    'path'          => '/' . $this->_getPathPart($list),
+                    'shadow_path'   => '/' . $list->getId(),
+                    'record_id'     => $list->getId(),
+                    'creation_time' => Tinebase_DateTime::now(),
+                )));
+            }
+
+            foreach ($list->memberroles as $role) {
+                if ($role->contact_id === $record->getId()) {
+                    $role = Addressbook_Controller_ListRole::getInstance()->get($role->list_role_id);
+                    foreach ($listPaths as $listPath) {
+                        $listPath->path .= '/' . $this->_getPathPart($role);
+                        $listPath->shadow_path .= '/' . $role->getId();
+                        $listPath->record_id = $role->getId();
+                    }
+                }
+            }
+
+            foreach ($listPaths as $listPath) {
+                $listPath->path .= '/' . $this->_getPathPart($record);
+                $listPath->shadow_path .= '/' .$record->getId();
+                $listPath->record_id = $record->getId();
+                $result->addRecord($listPath);
+            }
+        }
+
+        return $result;
+    }
 }
index 7fac0c4..45bfb8c 100644 (file)
@@ -450,4 +450,15 @@ class Addressbook_Controller_List extends Tinebase_Controller_Record_Abstract
         $result = $this->_getMemberRolesBackend()->getMultipleByProperty($record->getId(), 'list_id');
         return $result;
     }
+
+    /**
+     * get all lists given contact is member of
+     *
+     * @param $contact
+     * @return array
+     */
+    public function getMemberships($contact)
+    {
+        return $this->_backend->getMemberships($contact);
+    }
 }
index c219428..1e90786 100644 (file)
@@ -34,7 +34,29 @@ class Addressbook_Convert_Contact_Json extends Tinebase_Convert_Json
         }
         
         Addressbook_Frontend_Json::resolveImages($_records);
-        
-        return parent::fromTine20RecordSet($_records, $_filter, $_pagination);
+
+        $this->_appendRecordPaths($_records, $_filter);
+
+        $result = parent::fromTine20RecordSet($_records, $_filter, $_pagination);
+
+        return $result;
+    }
+
+    /**
+     * append record paths (if path filter is set)
+     *
+     * @param Tinebase_Record_RecordSet $_records
+     * @param Tinebase_Model_Filter_FilterGroup $_filter
+     *
+     * TODO move to generic json converter
+     */
+    protected function _appendRecordPaths($_records, $_filter)
+    {
+        if ($_filter && $_filter->getFilter('path', /* $_getAll = */ false, /* $_recursive = */ true) !== null) {
+            $recordPaths = Tinebase_Record_Path::getInstance()->getPathsForRecords($_records);
+            foreach ($_records as $record) {
+                $record->paths = $recordPaths->filter('record_id', $record->getId());
+            }
+        }
     }
 }
index f928379..5ea5b1f 100644 (file)
@@ -218,6 +218,7 @@ class Addressbook_Model_Contact extends Tinebase_Record_Abstract
             Zend_Filter_Input::DEFAULT_VALUE => self::CONTACTTYPE_CONTACT,
             array('InArray', array(self::CONTACTTYPE_USER, self::CONTACTTYPE_CONTACT)),
         ),
+        'paths'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
     );
     
     /**
@@ -430,4 +431,14 @@ class Addressbook_Model_Contact extends Tinebase_Record_Abstract
         $image = Tinebase_Controller::getInstance()->getImage('Addressbook', $this->getId());
         return $image->getBlob('image/jpeg', self::SMALL_PHOTO_SIZE);
     }
+
+    /**
+     * get title
+     *
+     * @return string
+     */
+    public function getTitle()
+    {
+        return $this->n_fn;
+    }
 }
index a63e099..6fb55fa 100644 (file)
@@ -44,6 +44,10 @@ class Addressbook_Model_ContactFilter extends Tinebase_Model_Filter_FilterGroup
             'filter' => 'Tinebase_Model_Filter_Query', 
             'options' => array('fields' => array('n_family', 'n_given', 'org_name', 'org_unit', 'email', 'email_home', 'adr_one_locality'))
         ),
+        'path'                => array(
+            'filter' => 'Tinebase_Model_Filter_Path',
+            'options' => array()
+        ),
         'list'                 => array('filter' => 'Addressbook_Model_ListMemberFilter'),
         'list_role_id'         => array('filter' => 'Addressbook_Model_ListRoleMemberFilter'),
         'telephone'            => array(
index 016570c..c507447 100644 (file)
@@ -106,6 +106,8 @@ Tine.Addressbook.SearchCombo = Ext.extend(Tine.Tinebase.widgets.form.RecordPicke
         if (this.userOnly) {
             this.store.baseParams.filter.push({field: 'type', operator: 'equals', value: 'user'});
         }
+
+        // TODO add OR filter with path here
     },
     
     /**
index e73f56f..97fa8d3 100644 (file)
         );
         
         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(
-            __METHOD__ . '::' . __LINE__ . " queueing action: '{$action}'");
+            __METHOD__ . '::' . __LINE__ . " Queueing action: '{$action}'");
         
         return $this->_queue->send($message);
     }
index f75bb2f..703c8fe 100644 (file)
@@ -175,6 +175,13 @@ abstract class Tinebase_Controller_Record_Abstract
     protected $_updateMultipleValidateEachRecord = FALSE;
 
     /**
+     * support record paths
+     *
+     * @var bool
+     */
+    protected $_useRecordPaths = false;
+
+    /**
      * returns controller for records of given model
      *
      * @param string $_model
@@ -1035,7 +1042,10 @@ abstract class Tinebase_Controller_Record_Abstract
     {
         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
             . ' Update record: ' . print_r($record->toArray(), true));
-        
+
+        $pathPartChanged = false;
+        $relationsTouched = false;
+
         // relations won't be touched if the property is set to NULL
         // an empty array on the relations property will remove all relations
         if ($record->has('relations') && isset($record->relations) && is_array($record->relations)) {
@@ -1048,7 +1058,43 @@ abstract class Tinebase_Controller_Record_Abstract
                 FALSE,
                 $this->_inspectRelatedRecords,
                 $this->_doRelatedCreateUpdateCheck);
+
+            $relationsTouched = true;
+
+        } elseif ($this->_getPathPart($updatedRecord) !== $this->_getPathPart($record)) {
+            $pathPartChanged = true;
         }
+
+        // rebuild paths if relations where set or if pathPart changed
+        if (true === $this->_useRecordPaths && (true === $pathPartChanged || true === $relationsTouched)) {
+
+            // rebuild own paths
+            $this->buildPath($record);
+
+            // rebuild paths of children that have been added or removed
+            if ($relationsTouched) {
+                //we need to do this to reload the relations in the next line...
+                $record->relations = null;
+                $newChildRelations = Tinebase_Relations::getInstance()->getRelationsOfRecordByDegree($record, Tinebase_Model_Relation::DEGREE_CHILD);
+                $oldChildRelations = Tinebase_Relations::getInstance()->getRelationsOfRecordByDegree($updatedRecord, Tinebase_Model_Relation::DEGREE_CHILD);
+
+                foreach ($newChildRelations as $relation) {
+                    $oldOffset = $oldChildRelations->getIndexById($relation->id);
+                    // new child
+                    if (false === $oldOffset) {
+                        $this->buildPath($relation->related_record);
+                    } else {
+                        $oldChildRelations->offsetUnset($oldOffset);
+                    }
+                }
+
+                //removed children
+                foreach ($oldChildRelations as $relation) {
+                    $this->buildPath($relation->related_record);
+                }
+            }
+        }
+
         if ($record->has('tags') && isset($record->tags) && (is_array($record->tags) || $record->tags instanceof Tinebase_Record_RecordSet)) {
             $updatedRecord->tags = $record->tags;
             Tinebase_Tags::getInstance()->setTagsOfRecord($updatedRecord);
@@ -2095,4 +2141,82 @@ abstract class Tinebase_Controller_Record_Abstract
     {
         
     }
+
+    /**
+     * returns path of record
+     *
+     * @param     $record
+     * @param int $depth
+     * @return Tinebase_Record_RecordSet
+     * @throws Tinebase_Exception_Record_NotAllowed
+     * @throws Tinebase_Exception
+     */
+    protected function _getPathsOfRecord($record, $depth = 0)
+    {
+        if ($depth > 8) {
+            throw new Tinebase_Exception('too many recursions while calculating record path');
+        }
+
+        $result = new Tinebase_Record_RecordSet('Tinebase_Model_Path');
+
+        $parentRelations = Tinebase_Relations::getInstance()->getRelationsOfRecordByDegree($record, Tinebase_Model_Relation::DEGREE_PARENT);
+        foreach ($parentRelations as $parent) {
+
+            // we do not need to generate the parents paths, they should be in DB
+            $parentPaths = Tinebase_Record_Path::getInstance()->getPathsForRecords($parent->related_record);
+
+            // this is redundant and should be removed, if parents paths are not correctly generated, it is inconsistent behavior:
+            // if parents paths are incorrectly not yet generated at all, we do it here
+            // but if parents paths are just incorrectly generated, but something is there, we accept that...
+            // better to remove this
+            //if (count($parentPaths) === 0) {
+            //    $parentPaths = $this->_getPathsOfRecord($parent->related_record, $depth + 1);
+            //}
+
+            if (count($parentPaths) === 0) {
+                $path = new Tinebase_Model_Path(array(
+                    'path'          => '/' .$this->_getPathPart($parent->related_record) . '/' . $this->_getPathPart($record),
+                    'shadow_path'   => '/' .$parent->related_id . '/' .$record->getId(),
+                    'record_id'     => $record->getId(),
+                    'creation_time' => Tinebase_DateTime::now(),
+                ));
+                $result->addRecord($path);
+            } else {
+                // merge paths
+                foreach ($parentPaths as $path) {
+                    $newPath = new Tinebase_Model_Path(array(
+                        'path'          => $path->path . '/' . $this->_getPathPart($record),
+                        'shadow_path'   => $path->shadow_path . '/' . $record->getId(),
+                        'record_id'     => $record->getId(),
+                        'creation_time' => Tinebase_DateTime::now(),
+                    ));
+
+                    $result->addRecord($newPath);
+                }
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * @param $record
+     * @return string
+     */
+    protected function _getPathPart($record)
+    {
+        return mb_substr(str_replace('/', '', trim($record->getTitle())), 0, 40);
+    }
+
+    /**
+     * shortcut to Tinebase_Record_Path::generatePathForRecord
+     *
+     * @param $record
+     */
+    public function buildPath($record)
+    {
+        if ($this->_useRecordPaths) {
+            Tinebase_Record_Path::getInstance()->generatePathForRecord($record);
+        }
+    }
 }
diff --git a/tine20/Tinebase/Model/Filter/Path.php b/tine20/Tinebase/Model/Filter/Path.php
new file mode 100644 (file)
index 0000000..f34e604
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+/**
+ * Tine 2.0
+ * 
+ * @package     Tinebase
+ * @subpackage  Filter
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2016 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ */
+
+/**
+ * Tinebase_Model_Filter_Path
+ * 
+ * filters own ids match result of path search
+ * 
+ * <code>
+ *      'contact'        => array('filter' => 'Tinebase_Model_Filter_Path', 'options' => array(
+ *      )
+ * </code>     
+ * 
+ * @package     Tinebase
+ * @subpackage  Filter
+ */
+class Tinebase_Model_Filter_Path extends Tinebase_Model_Filter_Text
+{
+    protected $_controller = null;
+
+    /**
+     * @var array
+     */
+    protected $_pathRecordIds = null;
+
+    /**
+     * get path controller
+     * 
+     * @return Tinebase_Record_Path
+     */
+    protected function _getController()
+    {
+        if ($this->_controller === null) {
+            $this->_controller = Tinebase_Record_Path::getInstance();
+        }
+        
+        return $this->_controller;
+    }
+    
+    /**
+     * appends sql to given select statement
+     *
+     * @param Zend_Db_Select                $_select
+     * @param Tinebase_Backend_Sql_Abstract $_backend
+     */
+    public function appendFilterSql($_select, $_backend)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' 
+            . 'Adding Path filter for: ' . $_backend->getModelName());
+        
+        $this->_resolvePathIds();
+
+        $idField = (isset($this->_options['idProperty']) || array_key_exists('idProperty', $this->_options)) ? $this->_options['idProperty'] : 'id';
+        $db = $_backend->getAdapter();
+        $qField = $db->quoteIdentifier($_backend->getTableName() . '.' . $idField);
+        if (empty($this->_pathRecordIds)) {
+            $_select->where('1=0');
+        } else {
+            $_select->where($db->quoteInto("$qField IN (?)", $this->_pathRecordIds));
+        }
+    }
+    
+    /**
+     * resolve foreign ids
+     */
+    protected function _resolvePathIds()
+    {
+        if (! is_array($this->_pathRecordIds)) {
+            // TODO this should be improved if it turns out to be a performance issue:
+            //  we only need the record_ids here and not complete records, so we could directly use the path sql backend
+            //  and just request the property we need
+            $this->_pathRecordIds = $this->_getController()->search(new Tinebase_Model_PathFilter(array(
+                array('field' => 'path', 'operator' => $this->_operator, 'value' => $this->_value)
+            )))->record_id;
+        }
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' foreign ids: ' 
+            . print_r($this->_pathRecordIds, TRUE));
+    }
+}
diff --git a/tine20/Tinebase/Model/Path.php b/tine20/Tinebase/Model/Path.php
new file mode 100644 (file)
index 0000000..7f5c0af
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * Tine 2.0
+ * 
+ * @package     Tinebase
+ * @subpackage  Record
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2016 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ * 
+ */
+
+/**
+ * class Tinebase_Model_Path
+ * 
+ * @package     Tinebase
+ * @subpackage  Record
+ */
+class Tinebase_Model_Path extends Tinebase_Record_Abstract 
+{
+    /**
+     * key in $_validators/$_properties array for the field which
+     *   represents the identifier
+     *
+     * @var string
+     */
+    protected $_identifier = 'id';
+
+    /**
+     * record validators
+     *
+     * @var array
+     */
+    protected $_validators = array(
+        'id'                => array('allowEmpty' => TRUE),
+        'record_id'         => array('allowEmpty' => TRUE),
+        'path'              => array('allowEmpty' => TRUE),
+        'shadow_path'       => array('allowEmpty' => TRUE),
+        'creation_time'     => array('allowEmpty' => TRUE),
+    );
+    
+    /**
+     * datetime fields
+     *
+     * @var array
+     */
+    protected $_datetimeFields = array(
+        'creation_time',
+    );
+}
diff --git a/tine20/Tinebase/Model/PathFilter.php b/tine20/Tinebase/Model/PathFilter.php
new file mode 100644 (file)
index 0000000..be350a0
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Tine 2.0
+ * 
+ * @package     Tinebase
+ * @subpackage  Filter
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ * @copyright   Copyright (c) 2016 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ *  persistent filter filter class
+ * 
+ * @package     Tinebase
+ * @subpackage  Filter 
+ */
+class Tinebase_Model_PathFilter extends Tinebase_Model_Filter_FilterGroup
+{
+    /**
+     * @var string application of this filter group
+     */
+    protected $_applicationName = 'Tinebase';
+    
+    /**
+     * @var string name of model this filter group is designed for
+     */
+    protected $_modelName = 'Tinebase_Model_Path';
+    
+    /**
+     * @var array filter model fieldName => definition
+     */
+    protected $_filterModel = array(
+        'id'             => array('filter' => 'Tinebase_Model_Filter_Id'),
+        'query'          => array('filter' => 'Tinebase_Model_Filter_Query', 'options' => array('fields' => array('path'))),
+        'record_id'      => array('filter' => 'Tinebase_Model_Filter_Id'),
+        'path'           => array('filter' => 'Tinebase_Model_Filter_Text'),
+    );
+}
diff --git a/tine20/Tinebase/Path/Backend/Sql.php b/tine20/Tinebase/Path/Backend/Sql.php
new file mode 100644 (file)
index 0000000..3b59be6
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Tinebase
+ * @subpackage  Path
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2016 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Paul Mehrer <p.mehrer@metaways.de>
+ *
+ */
+
+
+/**
+ * class Tinebase_Path_Backend_Sql
+ *
+ *
+ * @package     Tinebase
+ * @subpackage  Path
+ */
+class Tinebase_Path_Backend_Sql extends Tinebase_Backend_Sql_Abstract
+{
+    /**
+     * @var Zend_Db_Adapter_Abstract
+     */
+    protected $_db;
+
+    /**
+     * Table name without prefix
+     *
+     * @var string
+     */
+    protected $_tableName = 'path';
+
+    /**
+     * Model name
+     *
+     * @var string
+     */
+    protected $_modelName = 'Tinebase_Model_Path';
+
+    /**
+     * @param string $shadowPath
+     * @param string $replace
+     * @param string $substitution
+     * @return int          The number of affected rows.
+     */
+    public function replacePathForShadowPathTree($shadowPath, $replace, $substitution)
+    {
+        return $this->_db->update($this->_tablePrefix . $this->_tableName, array(
+            'path' => new Zend_Db_Expr($this->_db->quoteInto($this->_db->quoteInto('REPLACE(path, ?', $replace) . ', ?)', $substitution)),
+            ),
+            $this->_db->quoteInto($this->_db->quoteIdentifier('shadow_path') . ' like "?/%"', $shadowPath)
+        );
+    }
+
+    /**
+     * @param $shadowPath
+     * @return int          The number of affected rows.
+     */
+    public function deleteForShadowPathTree($shadowPath)
+    {
+        return $this->_db->delete($this->_tablePrefix . $this->_tableName,
+            $this->_db->quoteInto($this->_db->quoteIdentifier('shadow_path') . ' like "?/%"', $shadowPath)
+        );
+    }
+
+    /**
+     * @param $shadowPath
+     * @param $newPath
+     * @param $oldPath
+     * @param $newShadowPath
+     * @param $oldShadowPath
+     */
+    public function copyTreeByShadowPath($shadowPath, $newPath, $oldPath, $newShadowPath, $oldShadowPath)
+    {
+        $select = $this->_db->select()->from($this->_tablePrefix . $this->_tableName, array(
+                'path'          => new Zend_Db_Expr($this->_db->quoteInto($this->_db->quoteInto('REPLACE(path, ?', $oldPath) . ', ?)', $newPath)),
+                'shadow_path'   => new Zend_Db_Expr($this->_db->quoteInto($this->_db->quoteInto('REPLACE(shadow_path, ?', $oldShadowPath) . ', ?)', $newShadowPath)),
+                'record_id'     => 'record_id',
+                'creation_time' => new Zend_Db_Expr('NOW()'),
+            ))->where($this->_db->quoteInto($this->_db->quoteIdentifier('shadow_path') . ' like "?/%"', $shadowPath));
+        $stmt = $this->_db->query($select);
+        $entries = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
+
+        foreach($entries as $entry) {
+            $entry['id'] = Tinebase_Record_Abstract::generateUID();
+            $this->_db->insert($this->_tablePrefix . $this->_tableName, $entry);
+        }
+    }
+}
\ No newline at end of file
index 4e98fc4..413448e 100644 (file)
@@ -1195,7 +1195,8 @@ abstract class Tinebase_Record_Abstract implements Tinebase_Record_Interface
         
         // TODO: fallback, remove if all models use modelconfiguration
         if (! $c) {
-            return $this->title;
+            return $this->has('title') ? $this->title :
+                ($this->has('name') ? $this->name : $this->{$this->_identifier});
         }
         
         // use vsprintf formatting if it is an array
diff --git a/tine20/Tinebase/Record/Path.php b/tine20/Tinebase/Record/Path.php
new file mode 100644 (file)
index 0000000..5417629
--- /dev/null
@@ -0,0 +1,201 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Tinebase
+ * @subpackage  Record
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ * @copyright   Copyright (c) 2016 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ * controller for record paths
+ *
+ * @package     Tinebase
+ * @subpackage  Record
+ */
+class Tinebase_Record_Path extends Tinebase_Controller_Record_Abstract
+{
+    /**
+     * @var Tinebase_Backend_Sql
+     */
+    protected $_backend;
+    
+    /**
+     * Model name
+     *
+     * @var string
+     */
+    protected $_modelName = 'Tinebase_Model_Path';
+    
+    /**
+     * check for container ACLs?
+     *
+     * @var boolean
+     */
+    protected $_doContainerACLChecks = FALSE;
+    
+    /**
+     * holds the instance of the singleton
+     *
+     * @var Tinebase_Alarm
+     */
+    private static $instance = NULL;
+    
+    /**
+     * the constructor
+     *
+     */
+    private function __construct()
+    {
+        $this->_backend = new Tinebase_Path_Backend_Sql();
+    }
+    
+    /**
+     * the singleton pattern
+     *
+     * @return Tinebase_Record_Path
+     */
+    public static function getInstance() 
+    {
+        if (self::$instance === NULL) {
+            self::$instance = new self();
+        }
+        return self::$instance;
+    }
+
+    /**
+     * generates path for the record
+     *
+     * @param Tinebase_Record_Abstract $record
+     * @return Tinebase_Record_RecordSet
+     *
+     * TODO what about acl? the account who creates the path probably does not see all relations ...
+     */
+    public function generatePathForRecord($record)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . ' Generate path for ' . get_class($record) . ' record with id ' . $record->getId());
+
+        // fetch full record + check acl
+        $recordController = Tinebase_Core::getApplicationInstance(get_class($record));
+        $record = $recordController->get($record->getId());
+
+
+        $currentPaths = Tinebase_Record_Path::getInstance()->getPathsForRecords($record);
+
+        $newPaths = new Tinebase_Record_RecordSet('Tinebase_Model_Path');
+
+        // fetch all parent -> child relations and add to path
+        $newPaths->merge($this->_getPathsOfRecord($record));
+
+        if (method_exists($recordController, 'generatePathForRecord')) {
+            $newPaths->merge($recordController->generatePathForRecord($record));
+        }
+
+        //compare currentPaths with newPaths to find out if we need to make subtree updates
+        //we should do this before the new paths of the current record have been persisted to DB!
+        $currentShadowPathOffset = array();
+        foreach($currentPaths as $offset => $path) {
+            $currentShadowPathOffset[$path->shadow_path] = $offset;
+        }
+
+        $newShadowPathOffset = array();
+        foreach($newPaths as $offset => $path) {
+            $newShadowPathOffset[$path->shadow_path] = $offset;
+        }
+
+        $toDelete = array();
+        $anyOldOffset = null;
+        foreach($currentShadowPathOffset as $shadowPath => $offset) {
+            $anyOldOffset = $offset;
+
+            // parent path has been deleted!
+            if (false === isset($newShadowPathOffset[$shadowPath])) {
+                $toDelete[] = $shadowPath;
+                continue;
+            }
+
+            $currentPath = $currentPaths[$offset];
+            $newPath = $newPaths[$newShadowPathOffset[$shadowPath]];
+
+            // path changed (a title was updated or similar)
+            if ($currentPath->path !== $newPath->path) {
+                // update ... set path = REPLACE(path, $currentPath->path, $newPath->path) where shadow_path LIKE '$shadowPath/%'
+                $this->_backend->replacePathForShadowPathTree($shadowPath, $currentPath->path, $newPath->path);
+            }
+
+            unset($newShadowPathOffset[$shadowPath]);
+        }
+
+        // new parents
+        if (count($newShadowPathOffset) > 0 && null !== $anyOldOffset) {
+            $anyPath = $currentPaths[$anyOldOffset];
+            $newParents = array_values($newShadowPathOffset);
+            foreach ($newParents as $newParentOffset) {
+                $newParent = $newPaths[$newParentOffset];
+
+                // insert into ... select
+                // REPLACE(path, $anyPath->path, $newParent->path) as path,
+                // REPLACE(shadow_path, $anyPath->shadow_path, $newParent->shadow_path) as shadow_path
+                // from ... where shadow_path LIKE '$anyPath->shadow_path/%'
+                $this->_backend->copyTreeByShadowPath($anyPath->shadow_path, $newParent->path, $anyPath->path, $newParent->shadow_path, $anyPath->shadow_path);
+            }
+        }
+
+        //execute deletes only now, important to make 100% sure "new parents" just above still has data to work on!
+        foreach($toDelete as $delete) {
+            // delete where shadow_path LIKE '$delete/%'
+            $this->_backend->deleteForShadowPathTree($delete);
+        }
+
+
+        // delete current paths of this record
+        $this->deletePathsForRecord($record);
+
+
+        // recreate new paths of this record
+        foreach ($newPaths as $path) {
+            $this->_backend->create($path);
+        }
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . ' Created ' . count($newPaths) . ' paths.');
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . ' ' . print_r($newPaths->toArray(), true));
+
+
+        return $newPaths;
+    }
+
+    /**
+     * delete all record paths
+     *
+     * @param $record
+     * @return int
+     *
+     * TODO add acl check?
+     */
+    public function deletePathsForRecord($record)
+    {
+        return $this->_backend->deleteByProperty($record->getId(), 'record_id');
+    }
+
+    /**
+     * getPathsForRecords
+     *
+     * @param Tinebase_Record_Interface|Tinebase_Record_RecordSet $records
+     * @return Tinebase_Record_RecordSet
+     * @throws Tinebase_Exception_NotFound
+     */
+    public function getPathsForRecords($records)
+    {
+        $ids = $records instanceof Tinebase_Record_Interface ? array($records->getId()) : $records->getArrayOfIds();
+
+        return $this->search(new Tinebase_Model_PathFilter(array(
+            array('field' => 'record_id', 'operator' => 'in', 'value' => $ids)
+        )));
+    }
+}
index e20420b..bb8d70a 100644 (file)
@@ -671,6 +671,23 @@ class Tinebase_Record_RecordSet implements IteratorAggregate, Countable, ArrayAc
         
         return $this;
     }
+
+    /**
+     * merges records from given record set if id not yet present in current record set
+     *
+     * @param Tinebase_Record_RecordSet $_recordSet
+     * @return void
+     */
+    public function mergeById(Tinebase_Record_RecordSet $_recordSet)
+    {
+        foreach ($_recordSet as $record) {
+            if (false === $this->getIndexById($record->getId())) {
+                $this->addRecord($record);
+            }
+        }
+
+        return $this;
+    }
     
     /**
      * sorts this recordset
index 0b7e709..a5b24be 100644 (file)
@@ -651,7 +651,7 @@ class Tinebase_Relations
     public function transferRelations($sourceId, $destinationId, $model)
     {
         if (! Tinebase_Core::getUser()->hasRight('Tinebase', Tinebase_Acl_Rights::ADMIN)) {
-            throw new Tinebase_Exception_AccessDenied('Non admins of Tinebase aren\'t allowed to perform his operation!');
+            throw new Tinebase_Exception_AccessDenied('Only Admins are allowed to perform his operation!');
         }
         
         return $this->_backend->transferRelations($sourceId, $destinationId, $model);
@@ -680,4 +680,24 @@ class Tinebase_Relations
     {
         $this->_backend->removeApplication($applicationName);
     }
+
+    public function getRelationsOfRecordByDegree($record, $degree)
+    {
+        // get relations if not yet present OR use relation search here
+        if (empty($record->relations)) {
+            $backendType = 'Sql';
+            $modelName = get_class($record);
+            $record->relations = Tinebase_Relations::getInstance()->getRelations($modelName, $backendType, $record->getId());
+        }
+
+
+        $result = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
+        foreach ($record->relations as $relation) {
+            if ($relation->related_degree === $degree) {
+                $result->addRecord($relation);
+            }
+        }
+
+        return $result;
+    }
 }
index 8d93ce1..e806866 100644 (file)
@@ -109,7 +109,7 @@ class Tinebase_Setup_Update_Release8 extends Setup_Update_Abstract
      */
     protected function _addFilterAclTable()
     {
-        $xml = $declaration = new Setup_Backend_Schema_Table_Xml('<table>
+        $declaration = new Setup_Backend_Schema_Table_Xml('<table>
             <name>filter_acl</name>
             <version>1</version>
             <declaration>
index 339359f..ba8d2e5 100644 (file)
@@ -60,7 +60,6 @@ class Tinebase_Setup_Update_Release9 extends Setup_Update_Abstract
 
     /**
      * update to 9.5
-     *
      */
     public function update_4()
     {
@@ -95,7 +94,7 @@ class Tinebase_Setup_Update_Release9 extends Setup_Update_Abstract
             $this->_backend->alterCol('relations', $declaration, 'own_degree');
         }
 
-        // delte index unique-fields
+        // delete index unique-fields
         try {
             $this->_backend->dropIndex('relations', 'unique-fields');
         } catch (Exception $e) {}
@@ -132,4 +131,76 @@ class Tinebase_Setup_Update_Release9 extends Setup_Update_Abstract
         $this->setTableVersion('relations', '9');
         $this->setApplicationVersion('Tinebase', '9.6');
     }
+
+    /**
+     * update to 9.7
+     *
+     * @see 0011620: add "path" filter for records
+     */
+    public function update_6()
+    {
+        $declaration = new Setup_Backend_Schema_Table_Xml('<table>
+            <name>path</name>
+            <version>1</version>
+            <declaration>
+                <field>
+                    <name>id</name>
+                    <type>text</type>
+                    <length>40</length>
+                    <notnull>true</notnull>
+                </field>
+                <field>
+                    <name>record_id</name>
+                    <type>text</type>
+                    <length>40</length>
+                    <notnull>true</notnull>
+                </field>
+                <field>
+                    <name>path</name>
+                    <type>text</type>
+                    <length>255</length>
+                    <notnull>true</notnull>
+                </field>
+                <field>
+                    <name>shadow_path</name>
+                    <type>text</type>
+                    <length>255</length>
+                    <notnull>true</notnull>
+                </field>
+                <field>
+                    <name>creation_time</name>
+                    <type>datetime</type>
+                </field>
+                <index>
+                    <name>id</name>
+                    <primary>true</primary>
+                    <field>
+                        <name>id</name>
+                    </field>
+                </index>
+                <index>
+                    <name>path</name>
+                    <field>
+                        <name>path</name>
+                    </field>
+                </index>
+                <index>
+                    <name>shadow_path</name>
+                    <unique>true</unique>
+                    <field>
+                        <name>shadow_path</name>
+                    </field>
+                </index>
+                <index>
+                    <name>record_id</name>
+                    <field>
+                        <name>record_id</name>
+                    </field>
+                </index>
+            </declaration>
+        </table>');
+
+        $this->createTable('path', $declaration);
+        $this->setApplicationVersion('Tinebase', '9.7');
+    }
 }
index 15b6a90..a5d111c 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <application>
     <name>Tinebase</name>
-    <version>9.6</version>
+    <version>9.7</version>
     <tables>
         <table>
             <name>applications</name>
                 </index>
             </declaration>
         </table>
+        <table>
+            <name>path</name>
+            <version>1</version>
+            <declaration>
+                <field>
+                    <name>id</name>
+                    <type>text</type>
+                    <length>40</length>
+                    <notnull>true</notnull>
+                </field>
+                <field>
+                    <name>record_id</name>
+                    <type>text</type>
+                    <length>40</length>
+                    <notnull>true</notnull>
+                </field>
+                <field>
+                    <name>path</name>
+                    <type>text</type>
+                    <length>255</length>
+                    <notnull>true</notnull>
+                </field>
+                <field>
+                    <name>shadow_path</name>
+                    <type>text</type>
+                    <length>255</length>
+                    <notnull>true</notnull>
+                </field>
+                <field>
+                    <name>creation_time</name>
+                    <type>datetime</type>
+                </field>
+                <index>
+                    <name>id</name>
+                    <primary>true</primary>
+                    <field>
+                        <name>id</name>
+                    </field>
+                </index>
+                <index>
+                    <name>path</name>
+                    <field>
+                        <name>path</name>
+                    </field>
+                </index>
+                <index>
+                    <name>shadow_path</name>
+                    <unique>true</unique>
+                    <field>
+                        <name>shadow_path</name>
+                    </field>
+                </index>
+                <index>
+                    <name>record_id</name>
+                    <field>
+                        <name>record_id</name>
+                    </field>
+                </index>
+            </declaration>
+        </table>
     </tables>
 
     <defaultRecords>