0008840: relations config - constraints from the other side
authorAlexander Stintzing <a.stintzing@metaways.de>
Wed, 19 Feb 2014 18:09:24 +0000 (19:09 +0100)
committerPhilipp Schüle <p.schuele@metaways.de>
Wed, 10 Sep 2014 11:06:25 +0000 (13:06 +0200)
relation panel does not respect the constraints config
if defined on the side of the related_record.

 - validate in backend
 - nicer gui, better exception handling

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

Change-Id: I058277930004387a4a4ac4e21c589cbf73705daa
Reviewed-on: http://gerrit.tine20.com/customers/453
Tested-by: Jenkins CI (http://ci.tine20.com/)
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
Tested-by: Philipp Schüle <p.schuele@metaways.de>
21 files changed:
tests/tine20/Crm/JsonTest.php
tests/tine20/Sales/ControllerTest.php
tests/tine20/Sales/JsonTest.php
tests/tine20/Tinebase/Relation/RelationTest.php
tine20/Addressbook/Model/Contact.php
tine20/Crm/Model/Lead.php
tine20/Sales/Model/Contract.php
tine20/Tinebase/Backend/Sql/Command/Mysql.php
tine20/Tinebase/Backend/Sql/Command/Pgsql.php
tine20/Tinebase/Controller/Record/Abstract.php
tine20/Tinebase/Exception/InvalidRelationConstraints.php [new file with mode: 0644]
tine20/Tinebase/Frontend/Json/Abstract.php
tine20/Tinebase/Relation/Backend/Sql.php
tine20/Tinebase/Relations.php
tine20/Tinebase/css/Tinebase.css
tine20/Tinebase/js/ExceptionHandler.js
tine20/Tinebase/js/widgets/dialog/ExceptionHandlerDialog.js
tine20/Tinebase/js/widgets/dialog/MultipleEditDialogPlugin.js
tine20/Tinebase/js/widgets/relation/GenericPickerGridPanel.js
tine20/Tinebase/translations/de.po
tine20/Tinebase/translations/template.pot

index 8593e1c..a4a0da5 100644 (file)
@@ -364,7 +364,7 @@ class Crm_JsonTest extends Crm_AbstractTest
         $task = $this->_getTask();
         
         $taskJson = new Tasks_Frontend_Json();
-        $taskData = $this->_getTask()->toArray();
+        $taskData = $task->toArray();
         $taskData['relations'] = array(
             array(
                 'type'  => 'TASK',
@@ -377,6 +377,7 @@ class Crm_JsonTest extends Crm_AbstractTest
                 'related_record' => $leadData
             ),
         );
+        
         $taskData = $taskJson->saveTask($taskData);
         $taskData['description'] = 1;
         $taskData = $taskJson->saveTask($taskData);
@@ -396,6 +397,94 @@ class Crm_JsonTest extends Crm_AbstractTest
     }
     
     /**
+     * @see #8840: relations config - constraints from the other side
+     *      - validate in backend
+     *      
+     *      https://forge.tine20.org/mantisbt/view.php?id=8840
+     */
+    public function testConstraintsOtherSide()
+    {
+        $leadData1 = $this->_instance->saveLead($this->_getLead(FALSE, FALSE)->toArray());
+        $task = $this->_getTask();
+        
+        $taskJson = new Tasks_Frontend_Json();
+        $taskData = $task->toArray();
+        $taskData['relations'] = array(
+            array(
+                'type'  => 'TASK',
+                'own_model' => 'Tasks_Model_Task',
+                'own_backend' => 'Sql',
+                'own_degree' => 'sibling',
+                'related_model' => 'Crm_Model_Lead',
+                'related_backend' => 'Sql',
+                'related_id' => $leadData1['id'],
+                'related_record' => $leadData1
+            ),
+        );
+        
+        $taskData = $taskJson->saveTask($taskData);
+        
+        $leadData2 = $this->_instance->saveLead($this->_getLead(FALSE, FALSE)->toArray());
+        $taskData['relations'][] = array(
+            'type'  => 'TASK',
+            'own_model' => 'Tasks_Model_Task',
+            'own_backend' => 'Sql',
+            'own_degree' => 'sibling',
+            'related_model' => 'Crm_Model_Lead',
+            'related_backend' => 'Sql',
+            'related_id' => $leadData2['id'],
+            'related_record' => $leadData2
+        );
+        
+        $this->setExpectedException('Tinebase_Exception_InvalidRelationConstraints');
+        $taskJson->saveTask($taskData);
+    }
+    
+    public function testOtherRecordConstraintsConfig()
+    {
+        $leadData1 = $this->_instance->saveLead($this->_getLead(FALSE, FALSE)->toArray());
+        $task = $this->_getTask();
+        
+        $taskJson = new Tasks_Frontend_Json();
+        $leadJson = new Crm_Frontend_Json();
+        
+        $taskData = $task->toArray();
+        $taskData['relations'] = array(
+            array(
+                'type'  => 'TASK',
+                'own_model' => 'Tasks_Model_Task',
+                'own_backend' => 'Sql',
+                'own_degree' => 'sibling',
+                'related_model' => 'Crm_Model_Lead',
+                'related_backend' => 'Sql',
+                'related_id' => $leadData1['id'],
+                'related_record' => $leadData1
+            ),
+        );
+        
+        $taskData = $taskJson->saveTask($taskData);
+        
+        $leadData2 = $this->_instance->saveLead($this->_getLead(FALSE, FALSE)->toArray());
+        
+        $leadData2['relations'] = array(
+            array(
+                'type'  => 'TASK',
+                'own_model' => 'Crm_Model_Lead',
+                'own_backend' => 'Sql',
+                'own_degree' => 'sibling',
+                'related_model' => 'Tasks_Model_Task',
+                'related_backend' => 'Sql',
+                'related_id' => $taskData['id'],
+                'related_record' => $taskData
+            )
+        );
+        
+        $this->setExpectedException('Tinebase_Exception_InvalidRelationConstraints');
+        
+        $leadJson->saveLead($leadData2);
+    }
+    
+    /**
      * get contact
      * 
      * @return Addressbook_Model_Contact
@@ -466,16 +555,35 @@ class Crm_JsonTest extends Crm_AbstractTest
     /**
      * get lead
      * 
+     * @param boolean $addCf
+     * @param boolean $addTags
      * @return Crm_Model_Lead
      */
-    protected function _getLead()
+    protected function _getLead($addCf = TRUE, $addTags = TRUE)
     {
-        $cfc = Tinebase_CustomFieldTest::getCustomField(array(
-            'application_id' => Tinebase_Application::getInstance()->getApplicationByName('Crm')->getId(),
-            'model'          => 'Crm_Model_Lead',
-            'name'           => 'crmcf',
-        ));
-        Tinebase_CustomField::getInstance()->addCustomField($cfc);
+        if ($addCf) {
+            $cfc = Tinebase_CustomFieldTest::getCustomField(array(
+                'application_id' => Tinebase_Application::getInstance()->getApplicationByName('Crm')->getId(),
+                'model'          => 'Crm_Model_Lead',
+                'name'           => 'crmcf',
+            ));
+            
+            $cfs = array(
+                'crmcf' => '1234'
+            );
+            
+            Tinebase_CustomField::getInstance()->addCustomField($cfc);
+        } else {
+            $cfs = array();
+        }
+        
+        if ($addTags) {
+            $tags = array(
+                array('name' => 'lead tag', 'type' => Tinebase_Model_Tag::TYPE_SHARED)
+            );
+        } else {
+            $tags = array();
+        }
         
         return new Crm_Model_Lead(array(
             'lead_name'     => 'PHPUnit',
@@ -489,12 +597,8 @@ class Crm_JsonTest extends Crm_AbstractTest
             'turnover'      => 0,
             'probability'   => 70,
             'end_scheduled' => NULL,
-            'tags'          => array(
-                array('name' => 'lead tag', 'type' => Tinebase_Model_Tag::TYPE_SHARED)
-            ),
-            'customfields'  => array(
-                'crmcf' => '1234'
-            ),
+            'tags'          => $tags,
+            'customfields'  => $cfs
         ));
     }
     
index 9dcbc66..a626d44 100644 (file)
@@ -158,4 +158,4 @@ class Sales_ControllerTest extends PHPUnit_Framework_TestCase
             $numberBackend->update($number);
         }
     }
-}
\ No newline at end of file
+}
index e276e31..963390e 100644 (file)
@@ -95,45 +95,47 @@ class Sales_JsonTest extends PHPUnit_Framework_TestCase
      */
     public function testUpdateMultipleWithRelations()
     {
-        $contract1 = $this->_getContract();
-        $contract2 = $this->_getContract();
+        $contract1 = $this->_getContract('contract 1');
+        $contract2 = $this->_getContract('contract 2');
         $contract1 = Sales_Controller_Contract::getInstance()->create($contract1);
         $contract2 = Sales_Controller_Contract::getInstance()->create($contract2);
         
+        // peter, bob, laura, lisa
         list($contact1, $contact2, $contact3, $contact4) = $this->_createContacts();
         
         // add contact2 as customer relation to contract2
-        $this->_setContractRelations($contract2, array($contact2));
+        $this->_setContractRelations($contract2, array($contact2), 'CUSTOMER');
 
         $ids = array($contract1->id, $contract2->id);
 
-        $json = new Tinebase_Frontend_Json();
+        $tbJson = new Tinebase_Frontend_Json();
         // add Responsible contact1 to both contracts
-        $response = $json->updateMultipleRecords('Sales', 'Contract',
-            array(array('name' => '%CUSTOMER-Addressbook_Model_Contact', 'value' => $contact1->getId())),
+        $response = $tbJson->updateMultipleRecords('Sales', 'Contract',
+            array(array('name' => '%RESPONSIBLE-Addressbook_Model_Contact', 'value' => $contact1->getId())),
             array(array('field' => 'id', 'operator' => 'in', 'value' => $ids))
         );
 
-        $this->assertEquals(count($response['results']), 2);
+        $this->assertEquals(2, count($response['results']));
         
         $contract1re = $this->_instance->getContract($contract1->getId());
         $contract2re = $this->_instance->getContract($contract2->getId());
 
-        $this->assertEquals(count($contract1re['relations']), 1);
-        $this->assertEquals(count($contract2re['relations']), 2);
+        // only one CUSTOMER relation is allowed, contract2 still has related contact2
+        $this->assertEquals(1, count($contract1re['relations']), 'contract1 relations count failed: ' . print_r($contract1re, true));
+        $this->assertEquals(2, count($contract2re['relations']), 'contract2 relations count failed: ' . print_r($contract2re, true));
 
-        $this->assertEquals($contract1re['relations'][0]['related_id'], $contact1->getId());
+        $this->assertEquals($contact1->getId(), $contract1re['relations'][0]['related_id']);
         
-        if($contract2re['relations'][1]['related_id'] == $contact1->getId()) {
-            $this->assertEquals($contract2re['relations'][1]['related_id'], $contact1->getId());
-            $this->assertEquals($contract2re['relations'][0]['related_id'], $contact2->getId());
+        if ($contract2re['relations'][1]['related_id'] == $contact1->getId()) {
+            $this->assertEquals($contact1->getId(), $contract2re['relations'][1]['related_id']);
+            $this->assertEquals($contact2->getId(), $contract2re['relations'][0]['related_id']);
         } else {
-            $this->assertEquals($contract2re['relations'][1]['related_id'], $contact2->getId());
-            $this->assertEquals($contract2re['relations'][0]['related_id'], $contact1->getId());
+            $this->assertEquals($contact2->getId(), $contract2re['relations'][1]['related_id']);
+            $this->assertEquals($contact1->getId(), $contract2re['relations'][0]['related_id']);
         }
         
-        // update customer to contact3 and add responsible contact4
-        $response = $json->updateMultipleRecords('Sales', 'Contract',
+        // update customer to contact3 and add responsible to contact4, so contract1 and 2 will have 2 relations
+        $response = $tbJson->updateMultipleRecords('Sales', 'Contract',
             array(
                 array('name' => '%CUSTOMER-Addressbook_Model_Contact', 'value' => $contact3->getId()),
                 array('name' => '%RESPONSIBLE-Addressbook_Model_Contact', 'value' => $contact4->getId())
@@ -145,22 +147,22 @@ class Sales_JsonTest extends PHPUnit_Framework_TestCase
         $contract1re = $this->_instance->getContract($contract1->getId());
         $contract2re = $this->_instance->getContract($contract2->getId());
         
-        $this->assertEquals(count($contract1re['relations']), 2);
-        $this->assertEquals(count($contract2re['relations']), 3);
+        $this->assertEquals(2, count($contract1re['relations']));
+        $this->assertEquals(2, count($contract2re['relations']));
         
         // remove customer
-        $response = $json->updateMultipleRecords('Sales', 'Contract',
+        $response = $tbJson->updateMultipleRecords('Sales', 'Contract',
             array(array('name' => '%CUSTOMER-Addressbook_Model_Contact', 'value' => '')),
             array(array('field' => 'id', 'operator' => 'in', 'value' => $ids))
         );
         
-        $this->assertEquals(count($response['results']), 2);
+        $this->assertEquals(2, count($response['results']));
         
         $contract1res = $this->_instance->getContract($contract1->getId());
         $contract2res = $this->_instance->getContract($contract2->getId());
-        
-        $this->assertEquals(count($contract1res['relations']), 1);
-        $this->assertEquals(count($contract2res['relations']), 2);
+
+        $this->assertEquals(1, count($contract1res['relations']));
+        $this->assertEquals(1, count($contract2res['relations']));
     }
     
     /**
@@ -204,8 +206,9 @@ class Sales_JsonTest extends PHPUnit_Framework_TestCase
      * 
      * @param array|Sales_Model_Contract $contract
      * @param array $contacts
+     * @param string $type
      */
-    protected function _setContractRelations($contract, $contacts)
+    protected function _setContractRelations($contract, $contacts, $type = 'PARTNER')
     {
         $relationData = array();
         foreach ($contacts as $contact) {
@@ -215,7 +218,7 @@ class Sales_JsonTest extends PHPUnit_Framework_TestCase
                 'related_model' => 'Addressbook_Model_Contact',
                 'related_backend' => 'Sql',
                 'related_id' => $contact->getId(),
-                'type' => 'PARTNER'
+                'type' => $type
             );
         }
         $contractId = ($contract instanceof Sales_Model_Contract) ? $contract->getId() : $contract['id'];
@@ -383,11 +386,11 @@ class Sales_JsonTest extends PHPUnit_Framework_TestCase
      *
      * @return Sales_Model_Contract
      */
-    protected function _getContract()
+    protected function _getContract($title = 'phpunit contract', $desc = 'blabla')
     {
         return new Sales_Model_Contract(array(
-            'title'         => 'phpunit contract',
-            'description'   => 'blabla',
+            'title'         => $title,
+            'description'   => $desc,
         ), TRUE);
     }
 
@@ -530,6 +533,7 @@ class Sales_JsonTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(1, $ccs['totalcount']);
         $this->assertEquals(1, $ccs['results'][0]['is_deleted']);
     }
+
     
     /**
      * tests crud methods of division
@@ -563,4 +567,93 @@ class Sales_JsonTest extends PHPUnit_Framework_TestCase
         
         $d = $this->_instance->getDivision($d['id']);
     }
+        
+    /**
+     * @see https://forge.tine20.org/mantisbt/view.php?id=8840
+     */
+    public function testRelationConstraintsOwnSide()
+    {
+        $contract = Sales_Controller_Contract::getInstance()->create($this->_getContract());
+    
+        list($contact1, $contact2, $contact3, $contact4) = $this->_createContacts();
+        
+        $this->setExpectedException('Tinebase_Exception_InvalidRelationConstraints');
+        
+        $this->_setContractRelations($contract, array($contact1, $contact2), 'CUSTOMER');
+    }
+    
+    /**
+     * @see https://forge.tine20.org/mantisbt/view.php?id=8840
+     */
+    public function testRelationConstraintsOtherSide()
+    {
+        $contract = Sales_Controller_Contract::getInstance()->create($this->_getContract());
+        
+        list($contact1, $contact2, $contact3, $contact4) = $this->_createContacts(4);
+        
+        $this->_setContractRelations($contract, array($contact1), 'RESPONSIBLE');
+        
+        Addressbook_Controller_Contact::getInstance()->update($contact1);
+        $contact1 = Addressbook_Controller_Contact::getInstance()->get($contact1->getId(), NULL, TRUE);
+        $this->assertEquals(1, count($contact1->relations));
+        
+        // a partner may be added
+        $relation = new Tinebase_Model_Relation(array(
+            'own_degree' => 'sibling',
+            'own_model'  => 'Addressbook_Model_Contact',
+            'own_backend' => 'Sql',
+            'own_id' => $contact2->getId(),
+            'related_degree' => 'sibling',
+            'related_model' => 'Sales_Model_Contract',
+            'related_backend' => 'Sql',
+            'related_id' => $contract->getId(),
+            'type' => 'PARTNER'
+        ));
+        
+        $contact2->relations = array($relation);
+    
+        $contact2 = Addressbook_Controller_Contact::getInstance()->update($contact2);
+        $contact2 = Addressbook_Controller_Contact::getInstance()->get($contact2->getId(), NULL, TRUE);
+        $this->assertEquals(1, count($contact2->relations));
+        
+        // a second partner may be added also
+        $relation = new Tinebase_Model_Relation(array(
+            'own_degree' => 'sibling',
+            'own_model'  => 'Addressbook_Model_Contact',
+            'own_backend' => 'Sql',
+            'own_id' => $contact3->getId(),
+            'related_degree' => 'sibling',
+            'related_model' => 'Sales_Model_Contract',
+            'related_backend' => 'Sql',
+            'related_id' => $contract->getId(),
+            'type' => 'PARTNER'
+        ));
+        
+        $contact3->relations = array($relation);
+        Addressbook_Controller_Contact::getInstance()->update($contact3);
+        $contact3 = Addressbook_Controller_Contact::getInstance()->get($contact3->getId(), NULL, TRUE);
+        $this->assertEquals(1, count($contact3->relations));
+        
+        $contract = Sales_Controller_Contract::getInstance()->get($contract->getId(), NULL, TRUE);
+        $this->assertEquals(3, count($contract->relations));
+
+        // but a second responsible must not be added
+        $relation = new Tinebase_Model_Relation(array(
+            'own_degree' => 'sibling',
+            'own_model'  => 'Addressbook_Model_Contact',
+            'own_backend' => 'Sql',
+            'own_id' => $contact4->getId(),
+            'related_degree' => 'sibling',
+            'related_model' => 'Sales_Model_Contract',
+            'related_backend' => 'Sql',
+            'related_id' => $contract->getId(),
+            'type' => 'RESPONSIBLE'
+        ));
+        
+        $contact4->relations = array($relation);
+        
+        $this->setExpectedException('Tinebase_Exception_InvalidRelationConstraints');
+
+        $contact4 = Addressbook_Controller_Contact::getInstance()->update($contact4);
+    }
 }
index 264a210..41459c9 100644 (file)
@@ -353,13 +353,13 @@ class Tinebase_Relation_RelationTest extends TestCase
             'own_degree'     => Tinebase_Model_Relation::DEGREE_SIBLING,
             'related_model'  => 'Addressbook_Model_Contact',
             'related_record' => $sclever->toArray(),
-            'type'           => 'CUSTOMER',
+            'type'           => 'PARTNER',
         );
         $contract2Json['relations'][] = array(
             'own_degree'     => Tinebase_Model_Relation::DEGREE_SIBLING,
             'related_model'  => 'Addressbook_Model_Contact',
             'related_record' => $pwulf->toArray(),
-            'type'           => 'CUSTOMER',
+            'type'           => 'PARTNER',
         );
         $contract2Json = $json->saveContract($contract2Json);
         
@@ -380,5 +380,34 @@ class Tinebase_Relation_RelationTest extends TestCase
         
         Tinebase_Relations::getInstance()->transferRelations($sclever->getId(), $pwulf->getId(), 'Addressbook_Model_Contract');
     }
+    
+    /**
+     * tests if constraints config is called properly
+     * 
+     * @see #8840: relations config - constraints from the other side
+     *      - validate in backend
+     *      
+     *      https://forge.tine20.org/mantisbt/view.php?id=8840
+     */
+    public function testGetConstraintsConfigs() {
+        $result = Tinebase_Relations::getConstraintsConfigs('Sales_Model_Contract');
+        $this->assertEquals(10, count($result));
+        
+        foreach($result as $item) {
+            if ($item['ownRecordClassName'] == 'Sales_Model_Contract' && $item['relatedRecordClassName'] == 'Timetracker_Model_Timeaccount') {
+                $this->assertEquals('Contract', $item['ownModel']);
+                $this->assertEquals('Timeaccount', $item['relatedModel']);
+                $this->assertEquals('', $item['defaultType']);
+                $this->assertEquals('TIME_ACCOUNT', $item['config'][0]['type']);
+                $this->assertSame(0, $item['config'][0]['max']);
+            } elseif ($item['ownRecordClassName'] == 'Timetracker_Model_Timeaccount' && $item['relatedRecordClassName'] == 'Sales_Model_Contract') {
+                $this->assertEquals('Contract', $item['relatedModel']);
+                $this->assertEquals('Timeaccount', $item['ownModel']);
+                $this->assertEquals('TIME_ACCOUNT', $item['config'][0]['type']);
+                $this->assertEquals(TRUE, $item['reverted']);
+                $this->assertSame(1, $item['config'][0]['max']);
+            }
+        }
+    }
 }
 
index 5034a05..d6509a4 100644 (file)
@@ -126,7 +126,7 @@ class Addressbook_Model_Contact extends Tinebase_Record_Abstract
         'url'                   => array('StringTrim'),
         'url_home'              => array('StringTrim'),
     );
-    
+
     /**
      * list of zend validator
      * 
index a94f51d..82f8863 100644 (file)
@@ -115,14 +115,16 @@ class Crm_Model_Lead extends Tinebase_Record_Abstract
      * @see Tinebase_Record_Abstract
      */
     protected static $_relatableConfig = array(
+        // a lead may have one responsible and/or one customer
         array('relatedApp' => 'Addressbook', 'relatedModel' => 'Contact', 'config' => array(
             array('type' => 'RESPONSIBLE', 'degree' => 'parent', 'text' => 'Responsible', 'max' => '0:0'), // _('Responsible')
             array('type' => 'CUSTOMER', 'degree' => 'parent', 'text' => 'Customer', 'max' => '0:0'),  // _('Customer')
             ),
             'default' => array('type' => 'CUSTOMER', 'own_degree' => 'parent')
         ),
+        // a lead may have many tasks, but a task may have one lead, no more
         array('relatedApp' => 'Tasks', 'relatedModel' => 'Task', 'config' => array(
-            array('type' => 'TASK', 'degree' => 'sibling', 'text' => 'Task', 'max' => '1:0'), // _('Task')
+            array('type' => 'TASK', 'degree' => 'sibling', 'text' => 'Task', 'max' => '0:1'), // _('Task')
             ),
         )
     );
index df21a91..929d7f9 100644 (file)
@@ -100,6 +100,7 @@ class Sales_Model_Contract extends Tinebase_Record_Abstract
      * @see Tinebase_Record_Abstract
      */
     protected static $_relatableConfig = array(
+        // a contract may have one responsible and one customer but many partners
         array('relatedApp' => 'Addressbook', 'relatedModel' => 'Contact', 'config' => array(
             array('type' => 'RESPONSIBLE', 'degree' => 'sibling', 'text' => 'Responsible', 'max' => '1:0'), // _('Responsible')
             array('type' => 'CUSTOMER', 'degree' => 'sibling', 'text' => 'Customer', 'max' => '1:0'),  // _('Customer')
index 4d8df58..7acaf35 100644 (file)
@@ -43,6 +43,30 @@ class Tinebase_Backend_Sql_Command_Mysql implements Tinebase_Backend_Sql_Command
     }
 
     /**
+     * returns concatenation expression
+     * 
+     * @param array $values
+     */
+    public function getConcat($values)
+    {
+        $str = 'CONCAT(';
+        $i   = 1;
+        $vc  = count($values);
+        
+        foreach($values as $value) {
+            $str .= $value;
+            if ($i < $vc) {
+                $str .= ', ';
+            }
+            $i++;
+        }
+        
+        $str .= ')';
+        
+        return new Zend_Db_Expr($str);
+    }
+    
+    /**
      * @param string $field
      * @param mixed $returnIfTrue
      * @param mixed $returnIfFalse
index 835c1d4..5e0e182 100755 (executable)
@@ -45,7 +45,29 @@ class Tinebase_Backend_Sql_Command_Pgsql implements Tinebase_Backend_Sql_Command
         // before 9.0
         return new Zend_Db_Expr("array_to_string(ARRAY(SELECT DISTINCT unnest(array_agg($quotedField))),',')");
     }
-
+    
+    /**
+     * returns concatenation expression
+     *
+     * @param array $values
+     */
+    public function getConcat($values)
+    {
+        $str = '';
+        $i   = 1;
+        $vc  = count($values);
+        
+        foreach($values as $value) {
+            $str .= $value;
+            if ($i < $vc) {
+                $str .= ' || ';
+            }
+            $i++;
+        }
+        
+        return new Zend_Db_Expr($str);
+    }
+    
     /**
      * @param string $field
      * @param mixed $returnIfTrue
index 483d54f..b659016 100644 (file)
@@ -1044,56 +1044,73 @@ abstract class Tinebase_Controller_Record_Abstract
     
     /**
      * handles relations on update multiple
+     * 
      * @param string $key
      * @param string $value
      * @throws Tinebase_Exception_Record_DefinitionFailure
      */
-    protected function _handleRelations($key, $value)
+    protected function _handleRelationsOnUpdateMultiple($key, $value)
     {
-        $model = new $this->_modelName;
-        $relConfig = $model::getRelatableConfig();
-        unset($model);
         $getRelations = true;
         preg_match('/%(.+)-((.+)_Model_(.+))/', $key, $a);
-        if(count($a) < 4) {
+        if (count($a) < 4) {
             throw new Tinebase_Exception_Record_DefinitionFailure('The relation to delete/set is not configured properly!');
-        } 
-        // TODO: check config from foreign side
-        // $relConfig = $a[2]::getRelatableConfig();
-
-        $constrainsConfig = false;
-        foreach($relConfig as $config) {
-            if($config['relatedApp'] == $a[3] && $config['relatedModel'] == $a[4] && (isset($config['config']) || array_key_exists('config', $config)) && is_array($config['config'])) {
-                foreach($config['config'] as $constrain) {
-                    if($constrain['type'] == $a[1]) {
-                        $constrainsConfig = $constrain;
-                        break 2; 
+        }
+
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
+            Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Handle relations for ' . $this->_modelName);
+        }
+        
+        $relConfig = Tinebase_Relations::getConstraintsConfigs(array($this->_modelName, $a[2]));
+        
+        $constraintsConfig = NULL;
+        
+        if ($relConfig) {
+            foreach ($relConfig as $config) {
+                if ($config['relatedApp'] == $a[3] && $config['relatedModel'] == $a[4] && isset($config['config']) && is_array($config['config'])) {
+                    foreach ($config['config'] as $constraint) {
+                        if ($constraint['type'] == $a[1]) {
+                            $constraintsConfig = $constraint;
+                            break 2;
+                        }
                     }
                 }
             }
         }
-
-        if(!$constrainsConfig) {
+        
+        // update multiple is not possible without having a constraints config
+        if (! $constraintsConfig) {
             throw new Tinebase_Exception_Record_DefinitionFailure('No relation definition could be found for this model!');
         }
 
         $rel = array(
             'own_model' => $this->_modelName,
             'own_backend' => 'Sql',
-            'own_degree' =>(isset($constrainsConfig['sibling']) || array_key_exists('sibling', $constrainsConfig)) ? $constrainsConfig['sibling'] : 'sibling',
+            'own_degree' => isset($constraintsConfig['sibling']) ? $constraintsConfig['sibling'] : 'sibling',
             'related_model' => $a[2],
             'related_backend' => 'Sql',
-            'type' => (isset($constrainsConfig['type']) || array_key_exists('type', $constrainsConfig)) ? $constrainsConfig['type'] : '-',
-            'remark' => (isset($constrainsConfig['defaultRemark']) || array_key_exists('defaultRemark', $constrainsConfig)) ? $constrainsConfig['defaultRemark'] : ' '
+            'type' => isset($constraintsConfig['type']) ? $constraintsConfig['type'] : ' ',
+            'remark' => isset($constraintsConfig['defaultRemark']) ? $constraintsConfig['defaultRemark'] : ' '
         );
         
-        if(empty($value)) { // delete relations in iterator
-            if(!$this->_removeRelations) $this->removeRelations = array();
+        if (! $this->_removeRelations) {
+            $this->_removeRelations = array($rel);
+        } else {
             $this->_removeRelations[] = $rel;
-        } else { // create relations in iterator
-            if(! $this->_newRelations) $this->_newRelations = array();
+        }
+        
+        if (! empty($value)) { // delete relations in iterator
             $rel['related_id'] = $value;
-            $this->_newRelations[] = $rel;
+            if (! $this->_newRelations) {
+                $this->_newRelations = array($rel);
+            } else {
+                $this->_newRelations[] = $rel;
+            }
+        }
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) {
+            Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' New relations: ' . print_r($this->_newRelations, true)
+               . ' Remove relations: ' . print_r($this->_removeRelations, true));
         }
     }
     /**
@@ -1121,7 +1138,7 @@ abstract class Tinebase_Controller_Record_Abstract
             }
             if (stristr($key, '%')) {
                 $getRelations = true;
-                $this->_handleRelations($key, $value);
+                $this->_handleRelationsOnUpdateMultiple($key, $value);
                 unset($_data[$key]);
             }
         }
@@ -1142,7 +1159,9 @@ abstract class Tinebase_Controller_Record_Abstract
         ));
         $result = $iterator->iterate($_data);
     
-        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updated ' . $this->_updateMultipleResult['totalcount'] . ' records.');
+        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
+            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updated ' . $this->_updateMultipleResult['totalcount'] . ' records.');
+        }
         
         if ($this->_clearCustomFieldsCache) {
             Tinebase_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array('customfields'));
@@ -1175,37 +1194,45 @@ abstract class Tinebase_Controller_Record_Abstract
             $currentRecord->relations = new Tinebase_Record_RecordSet('Tinebase_Model_Relation');
         }
         
+        $be = new Tinebase_Relation_Backend_Sql();
+
         // handle relations to remove
         if($this->_removeRelations) {
             if($currentRecord->relations->count()) {
                 foreach($this->_removeRelations as $remRelation) {
-                    $removeRelations = $currentRecord->relations->filter('type', $remRelation['type']);
-                    $removeRelations = $removeRelations->filter('related_model', $remRelation['related_model']);
-                    $removeRelations = $removeRelations->filter('own_degree', $remRelation['own_degree']);
+                    $removeRelations = $currentRecord->relations
+                        ->filter('type', $remRelation['type'])
+                        ->filter('related_model', $remRelation['related_model']);
+                    
                     $currentRecord->relations->removeRecords($removeRelations);
                 }
             }
         }
-        
+
         // handle new relations
         if($this->_newRelations) {
             $removeRelations = NULL;
             foreach($this->_newRelations as $newRelation) {
-                $removeRelations = $currentRecord->relations->filter('type', $newRelation['type']);
-                $removeRelations = $removeRelations->filter('related_model', $newRelation['related_model']);
-                $removeRelations = $removeRelations->filter('own_degree', $newRelation['own_degree']);
+                $removeRelations = $currentRecord->relations
+                    ->filter('type', $newRelation['type'])
+                    ->filter('related_model', $newRelation['related_model']);
+                
                 $already = $removeRelations->filter('related_id', $newRelation['related_id']);
+                
                 if($already->count() > 0) {
                     $removeRelations = NULL;
                 } else {
                     $newRelation['own_id'] = $currentRecord->getId();
                     $rel = new Tinebase_Model_Relation();
                     $rel->setFromArray($newRelation);
-                    if($removeRelations) $currentRecord->relations->removeRecords($removeRelations);
+                    if ($removeRelations) {
+                        $currentRecord->relations->removeRecords($removeRelations);
+                    }
                     $currentRecord->relations->addRecord($rel);
                 }
             }
         }
+        
         return $currentRecord->relations->toArray();
     }
     
@@ -1228,7 +1255,7 @@ abstract class Tinebase_Controller_Record_Abstract
             
             $data = array_merge($oldRecordArray, $_data);
             
-            if($this->_newRelations || $this->_removeRelations) {
+            if ($this->_newRelations || $this->_removeRelations) {
                 $data['relations'] = $this->_iterateRelations($currentRecord);
             }
             try {
diff --git a/tine20/Tinebase/Exception/InvalidRelationConstraints.php b/tine20/Tinebase/Exception/InvalidRelationConstraints.php
new file mode 100644 (file)
index 0000000..42f8bc7
--- /dev/null
@@ -0,0 +1,39 @@
+<?php
+/**
+ * Tine 2.0
+ * 
+ * @package     Tinebase
+ * @subpackage  Exception
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @copyright   Copyright (c) 2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Alexander Stintzing <a.stintzing@metaways.de>
+ *
+ */
+
+/**
+ * Tinebase exception with exception data
+ * 
+ * @package     Tinebase
+ * @subpackage  Exception
+ */
+class Tinebase_Exception_InvalidRelationConstraints extends Tinebase_Exception
+{
+    /**
+     * the title of the Exception (may be shown in a dialog)
+     *
+     * @var string
+     */
+    protected $_title = 'Invalid Relations'; // _('Invalid Relations')
+    
+    /**
+     * construct
+     *
+     * @param string $_message
+     * @param integer $_code
+     * @return void
+     */
+     public function __construct($_message = "You tried to create a relation which is forbidden by the constraints config of one of the models.", $_code = 912) {
+        // _("You tried to create a relation which is forbidden by the constraints config of one of the models.")
+            parent::__construct($_message, $_code);
+    }
+}
index 7928f2b..c8d2ead 100644 (file)
@@ -66,77 +66,17 @@ abstract class Tinebase_Frontend_Json_Abstract extends Tinebase_Frontend_Abstrac
     {
         if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ 
                     . ' This method is deprecated and will be removed. Please use Tinebase_ModelFactory with Tinebase_ModelConfiguration!');
-        $ret = array();
         
         if (property_exists($this, '_relatableModels') && is_array($this->_relatableModels)) {
-            // iterates relatable models of this app
-            foreach ($this->_relatableModels as $model) {
-                
-                $ownModel = explode('_Model_', $model);
-                
-                if (! class_exists($model) || $ownModel[0] != $this->_applicationName) {
-                    continue;
-                }
-                $cItems = $model::getRelatableConfig();
-                
-                $ownModel = $ownModel[1];
-                
-                if (is_array($cItems)) {
-                    foreach($cItems as $cItem) {
-                        
-                        if (! (isset($cItem['config']) || array_key_exists('config', $cItem))) {
-                            continue;
-                        }
-                        
-                        // own side
-                        $ownConfigItem = $cItem;
-                        $ownConfigItem['ownModel'] = $ownModel;
-                        $ownConfigItem['ownApp'] = $this->_applicationName;
-                        
-                        $foreignConfigItem = array(
-                            'reverted'     => true,
-                            'ownApp'       => $cItem['relatedApp'],
-                            'ownModel'     => $cItem['relatedModel'], 
-                            'relatedModel' => $ownModel,
-                            'relatedApp'   => $this->_applicationName,
-                            'default'      => (isset($cItem['default']) || array_key_exists('default', $cItem)) ? $cItem['default'] : NULL
-                        );
-                        
-                        // KeyfieldConfigs
-                        if ((isset($cItem['keyfieldConfig']) || array_key_exists('keyfieldConfig', $cItem))) {
-                            $foreignConfigItem['keyfieldConfig'] = $cItem['keyfieldConfig'];
-                            if ($cItem['keyfieldConfig']['from']){
-                                $foreignConfigItem['keyfieldConfig']['from'] = $cItem['keyfieldConfig']['from'] == 'foreign' ? 'own' : 'foreign';
-                            }
-                        }
-                        
-                        $j=0;
-                        foreach ($cItem['config'] as $conf) {
-                            $max = explode(':',$conf['max']);
-                            $ownConfigItem['config'][$j]['max'] = $max[0];
-                            
-                            $foreignConfigItem['config'][$j] = $conf;
-                            $foreignConfigItem['config'][$j]['max'] = $max[1];
-                            if ($conf['degree'] == 'sibling') {
-                                $foreignConfigItem['config'][$j]['degree'] = $conf['degree'];
-                            } else {
-                                $foreignConfigItem['config'][$j]['degree'] = $conf['degree'] == 'parent' ? 'child' : 'parent';
-                            }
-                            $j++;
-                        }
-                        
-                        $ret[] = $ownConfigItem;
-                        $ret[] = $foreignConfigItem;
-                    }
-                }
-            }
+            return Tinebase_Relations::getConstraintsConfigs($this->_relatableModels);
+        } else {
+            return array();
         }
-        
-        return $ret;
     }
     
     /**
      * returns model configurations for application starter
+     * 
      * @return array
      */
     public function getModelsConfiguration()
@@ -153,15 +93,16 @@ abstract class Tinebase_Frontend_Json_Abstract extends Tinebase_Frontend_Abstrac
     }
     
     /**
-     * returns the default model
-     * @return NULL
+     * returns the default model or null if it does not exist
+     * 
+     * @return string
      */
     public function getDefaultModel()
     {
-        if ($this->_defaultModel) {
+        if (is_string($this->_defaultModel)) {
             return $this->_defaultModel;
         }
-        if ($this->_configuredModels) {
+        if ($this->_configuredModels && is_array($this->_configuredModels) && count($this->_configuredModels) > 0) {
             return $this->_configuredModels[0];
         }
         return NULL;
index e37eb6a..1b55640 100644 (file)
@@ -29,7 +29,7 @@
  * @package     Tinebase
  * @subpackage  Relations
  */
-class Tinebase_Relation_Backend_Sql
+class Tinebase_Relation_Backend_Sql extends Tinebase_Backend_Sql_Abstract
 {
     /**
      * @var Zend_Db_Adapter_Abstract
@@ -49,6 +49,7 @@ class Tinebase_Relation_Backend_Sql
     public function __construct()
     {
         $this->_db = Tinebase_Core::getDb();
+        $this->_dbCommand = Tinebase_Backend_Sql_Command::factory($this->_db);
         
         // temporary on the fly creation of table
         $this->_dbTable = new Tinebase_Db_Table(array(
@@ -372,8 +373,8 @@ class Tinebase_Relation_Backend_Sql
         $controller = Tinebase_Controller_Record_Abstract::getController($model);
         
         // just for validation, the records aren't needed
-        $sourceRecord      = $controller->get($sourceId);
-        $destinationRecord = $controller->get($destinationId);
+        $controller->get($sourceId);
+        $controller->get($destinationId);
         
         $tableName = SQL_TABLE_PREFIX . 'relations';
         
@@ -435,4 +436,41 @@ class Tinebase_Relation_Backend_Sql
 
         return $skipped;
     }
+
+    /**
+     * counts related records, gropued by Model, Type and Id but excludes relations which will be updated by $excludeCount
+     *
+     * @param string $ownModel
+     * @param Tinebase_Record_RecordSet $relations
+     * @return array
+     */
+    public function countRelatedConstraints($ownModel, $relations, $excludeCount)
+    {
+        if ($relations->count() == 0) {
+            return array();
+        }
+    
+        $adapter = $this->_dbTable->getAdapter();
+        $tableName = SQL_TABLE_PREFIX . 'relations';
+        
+        $sql = 'SELECT '. $this->_dbCommand->getConcat(array($this->_db->quoteIdentifier('related_model'), "'--'", $this->_db->quoteIdentifier('type'), "'--'", $this->_db->quoteIdentifier('own_id'))) . ' 
+                    AS ' . $this->_db->quoteIdentifier('id') . ',
+                    ' . $this->_db->quoteIdentifier('related_model') .', ' . $this->_db->quoteIdentifier('type') .',
+                    ' . $this->_db->quoteIdentifier('own_model') .', COUNT(*)
+                    AS ' . $this->_db->quoteIdentifier('count') . '
+                FROM ' . $this->_db->quoteIdentifier($tableName) . '
+                WHERE ' . $this->_db->quoteInto($this->_db->quoteIdentifier('own_id') . ' IN (?) ', $relations->related_id) . '
+                    AND '. $this->_db->quoteInto($this->_db->quoteIdentifier('related_model'). ' = ? ', $ownModel) . '
+                    AND '. $this->_db->quoteIdentifier('is_deleted'). ' = 0 ';
+        
+        if (! empty($excludeCount)) {
+            $sql .= ' AND '. $this->_db->quoteInto($this->_db->quoteIdentifier('id'). ' NOT IN (?) ', $excludeCount);
+        }
+        
+        $sql .= 'GROUP BY '. $this->_db->quoteIdentifier('own_id') .','.$this->_db->quoteIdentifier('related_model') . ', ' . $this->_db->quoteIdentifier('own_model') . ', ' . $this->_db->quoteIdentifier('type') . ', ' . $this->_db->quoteIdentifier('related_id');
+
+        $result = $adapter->fetchAssoc($sql);
+    
+        return $result;
+    }
 }
index 8a015d9..96e0e82 100644 (file)
@@ -97,11 +97,13 @@ class Tinebase_Relations
         $toDel = array_diff($currentIds, $relationsIds);
         $toUpdate = array_intersect($currentIds, $relationsIds);
         
+        $this->_validateConstraintsConfig($_model, $relations, $toDel, $toUpdate);
+        
         // break relations
         foreach ($toDel as $relationId) {
             $this->_backend->breakRelation($relationId);
         }
-
+        
         // add new relations
         foreach ($toAdd as $idx) {
             if(empty($relations[$idx]->related_id)) {
@@ -147,6 +149,175 @@ class Tinebase_Relations
     }
     
     /**
+     * returns the constraints config for the given models and their mirrored values (seen from the other side
+     * 
+     * @param array $models
+     * @return array
+     */
+    public static function getConstraintsConfigs($models)
+    {
+        if (! is_array($models)) {
+            $models = array($models);
+        }
+        $allApplications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED)->name;
+        $ret = array();
+        
+        foreach ($models as $model) {
+        
+            $ownModel = explode('_Model_', $model);
+        
+            if (! class_exists($model) || ! in_array($ownModel[0], $allApplications)) {
+                continue;
+            }
+            $cItems = $model::getRelatableConfig();
+            
+            $ownApplication = $ownModel[0];
+            $ownModel = $ownModel[1];
+        
+            if (is_array($cItems)) {
+                foreach($cItems as $cItem) {
+        
+                    if (! array_key_exists('config', $cItem)) {
+                        continue;
+                    }
+        
+                    // own side
+                    $ownConfigItem = $cItem;
+                    $ownConfigItem['ownModel'] = $ownModel;
+                    $ownConfigItem['ownApp'] = $ownApplication;
+                    $ownConfigItem['ownRecordClassName'] = $ownApplication . '_Model_' . $ownModel;
+                    $ownConfigItem['relatedRecordClassName'] = $cItem['relatedApp'] . '_Model_' . $cItem['relatedModel'];
+                    
+                    $foreignConfigItem = array(
+                        'reverted'     => true,
+                        'ownApp'       => $cItem['relatedApp'],
+                        'ownModel'     => $cItem['relatedModel'],
+                        'relatedModel' => $ownModel,
+                        'relatedApp'   => $ownApplication,
+                        'default'      => array_key_exists('default', $cItem) ? $cItem['default'] : NULL,
+                        'ownRecordClassName' => $cItem['relatedApp'] . '_Model_' . $cItem['relatedModel'],
+                        'relatedRecordClassName' => $ownApplication . '_Model_' . $ownModel
+                    );
+        
+                    // KeyfieldConfigs
+                    if (array_key_exists('keyfieldConfig', $cItem)) {
+                        $foreignConfigItem['keyfieldConfig'] = $cItem['keyfieldConfig'];
+                        if ($cItem['keyfieldConfig']['from']){
+                            $foreignConfigItem['keyfieldConfig']['from'] = $cItem['keyfieldConfig']['from'] == 'foreign' ? 'own' : 'foreign';
+                        }
+                    }
+        
+                    $j=0;
+                    foreach ($cItem['config'] as $conf) {
+                        $max = explode(':',$conf['max']);
+                        $ownConfigItem['config'][$j]['max'] = intval($max[0]);
+        
+                        $foreignConfigItem['config'][$j] = $conf;
+                        $foreignConfigItem['config'][$j]['max'] = intval($max[1]);
+                        if ($conf['degree'] == 'sibling') {
+                            $foreignConfigItem['config'][$j]['degree'] = $conf['degree'];
+                        } else {
+                            $foreignConfigItem['config'][$j]['degree'] = $conf['degree'] == 'parent' ? 'child' : 'parent';
+                        }
+                        $j++;
+                    }
+                    
+                    $ret[] = $ownConfigItem;
+                    $ret[] = $foreignConfigItem;
+                }
+            }
+        }
+        
+        return $ret;
+    }
+    
+    /**
+     * validate constraints from the own and the other side.
+     * this may be very expensive, if there are many constraints to check.
+     * 
+     * @param string $ownModel
+     * @param Tinebase_Record_RecordSet $relations
+     * @throws Tinebase_Exception_InvalidRelationConstraints
+     */
+    protected function _validateConstraintsConfig($ownModel, $relations, $toDelete = array(), $toUpdate = array())
+    {
+        if (! $relations->count()) {
+            return;
+        }
+        $relatedModels = array_unique($relations->related_model);
+        $relatedIds    = array_unique($relations->related_id);
+        
+        $toDelete      = is_array($toDelete) ? $toDelete : array();
+        $toUpdate      = is_array($toUpdate) ? $toUpdate : array();
+        $excludeCount  = array_merge($toDelete, $toUpdate);
+
+        $ownId         = $relations->getFirstRecord()->own_id;
+        $ownConfig     = $ownModel::getRelatableConfig();
+        
+        // find out all models having a constraints config
+        $allModels = $relatedModels;
+        $allModels[] = $ownModel;
+        $allModels = array_unique($allModels);
+
+        $constraintsConfigs = self::getConstraintsConfigs($allModels);
+        $relatedConstraints = $this->_backend->countRelatedConstraints($ownModel, $relations, $excludeCount);
+        
+        $newConstraints = $myConstraints = array();
+        
+        $groups = array();
+        foreach($relations as $relation) {
+            $groups[] = $relation->related_model . '--' . $relation->type . '--' . $relation->own_id;
+        }
+        
+        $myConstraints = array_count_values($groups);
+
+        $groups = array();
+        foreach($relations as $relation) {
+            if (! in_array($relation->getId(), $excludeCount)) {
+                $groups[] = $relation->own_model . '--' . $relation->type . '--' . $relation->related_id;
+            }
+        }
+        
+        foreach($relatedConstraints as $relC) {
+            for ($i = 0; $i < $relC['count']; $i++) {
+                $groups[] = $relC['id'];
+            }
+        }
+        
+        $allConstraints = array_count_values($groups);
+
+        foreach($constraintsConfigs as $cc) {
+            foreach($cc['config'] as $config) {
+                
+                $group = $cc['relatedRecordClassName'] . '--' . $config['type'];
+                $idGroup = $group . '--' . $ownId;
+
+                if (isset($myConstraints[$idGroup]) && ($config['max'] > 0 && $config['max'] < $myConstraints[$idGroup])) {
+                
+                    if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
+                        Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Constraints validation failed from the own side! ' . print_r($cc, 1));
+                    }
+                    throw new Tinebase_Exception_InvalidRelationConstraints();
+                }
+                
+                // TODO: if the other side gets the config reverted here, validating constrains failes here on multiple update 
+                foreach($relatedIds as $relatedId) {
+                    $idGroup = $group . '--' . $relatedId;
+                    
+                    if (isset($allConstraints[$idGroup]) && ($config['max'] > 0 && $config['max'] < $allConstraints[$idGroup])) {
+                        
+                        if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
+                            Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Constraints validation failed from the other side! ' . print_r($cc, 1));
+                        }
+
+                        throw new Tinebase_Exception_InvalidRelationConstraints();
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
      * get all relations of a given record
      * - cache result if caching is activated
      * 
index 6b2c74d..2478055 100644 (file)
@@ -742,10 +742,12 @@ html, body {
     background-image:url("../../library/ExtJS/resources/images/default/s.gif");
 }
 
-.tine-editorgrid-row-invalid {
-    border: 1px dotted red;
+.tine-editorgrid-row-invalid, .x-grid3-row-selected .tine-editorgrid-row-invalid {
+    border: 1px dotted red!important;
 }
 
+x-grid3-row   x-grid3-dirty-row tine-editorgrid-row-invalid x-grid3-row-first  
+
 .tine-duplicateresolve-keepvalue {
     color: #006600;
 }
index ebc2597..512a5ad 100644 (file)
@@ -276,7 +276,7 @@ Tine.Tinebase.ExceptionHandler = function() {
                 break;
                 
             // Tinebase_Exception_InvalidRelationConstraints
-            case 913
+            case 912
                 Ext.MessageBox.show(Ext.apply(defaults, {
                     title: _(exception.title),
                     msg: _(exception.message)
index aa6d5d0..ccbce22 100644 (file)
@@ -98,10 +98,10 @@ Tine.widgets.dialog.ExceptionHandlerDialog = Ext.extend(Ext.FormPanel, {
         this.app = Tine.Tinebase.appMgr.get(this.exception.appName);
         this.message = this.app.i18n._hidden(this.exception.message);
         
-        this.callbackOnOk = this.callbackOnOk ? this.callbackOnOk : this.callback ? this.callback : this.onClose;
-        this.callbackOnCancel = this.callbackOnCancel ? this.callbackOnCancel : this.callback ? this.callback : this.onClose;
-        this.callbackOnCancelScope = this.callbackOnCancelScope ? this.callbackOnCancelScope : this.callbackScope ? this.callbackScope : this;
-        this.callbackOnOkScope = this.callbackOnOkScope ? this.callbackOnOkScope : this.callbackScope ? this.callbackScope : this;
+        this.callbackOnOk = this.callbackOnOk ? this.callbackOnOk : (this.callback ? this.callback : this.onClose);
+        this.callbackOnCancel = this.callbackOnCancel ? this.callbackOnCancel : (this.callback ? this.callback : this.onClose);
+        this.callbackOnCancelScope = this.callbackOnCancelScope ? this.callbackOnCancelScope : (this.callbackScope ? this.callbackScope : this);
+        this.callbackOnOkScope = this.callbackOnOkScope ? this.callbackOnOkScope : (this.callbackScope ? this.callbackScope : this);
         
         this.initActions();
         this.initButtons();
index 8e32ce6..48a55c0 100644 (file)
@@ -360,14 +360,21 @@ Tine.widgets.dialog.MultipleEditDialogPlugin.prototype = {
         
         if (this.multiButton.hasClass('undo')) {
             Tine.log.debug('Resetting value to "' + this.startingValue + '".');
+            
             if (this.startRecord) {
+                
                 this.store.removeAll();
-                this.setValue(this.startRecord);
-                this.value = this.startingValue;
+                this.reset();
+                
+                if (this.multi) {
+                    this.setValue('');
+                } else {
+                    this.setValue(this.startRecord);
+                    this.value = this.startingValue;
+                }
             } else {
                 this.setValue(this.startingValue);
             }
-            this.clearInvalid();
             
             if (this.isXType('extuxclearabledatefield') && this.multi) {
                 var startLeft = this.startLeft ? this.startLeft : 0;
@@ -385,6 +392,9 @@ Tine.widgets.dialog.MultipleEditDialogPlugin.prototype = {
                 var parent = this.el.parent().select('.tinebase-editmultipledialog-clearer');
                 parent.setStyle('left', startLeft + 'px');
             }
+            if (this.store) {
+                this.store.removeAll();
+            }
             this.setValue('');
             if (this.multi) {
                 this.cleared = true;
@@ -709,6 +719,9 @@ Tine.widgets.dialog.MultipleEditDialogPlugin.prototype = {
                                 this.editDialog.onCancel();
                             }
                         },
+                        failure : function(exception) {
+                            Tine.Tinebase.ExceptionHandler.handleRequestException(exception, this.onUpdateFailure, this);
+                        },
                         scope: this
                     });
                 }
@@ -720,6 +733,16 @@ Tine.widgets.dialog.MultipleEditDialogPlugin.prototype = {
     },
     
     /**
+     * 
+     * @param {} btn
+     * @param {} dialog
+     */
+    onUpdateFailure: function(btn, dialog) {
+        this.form.clear();
+        this.onRecordLoad();
+    },
+    
+    /**
      * fetch records from backend on selectionFilter
      */
     fetchRecordsOnLoad: function() {
index cbc605a..a85ba61 100644 (file)
@@ -4,7 +4,7 @@
  * @package     Tinebase
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Alexander Stintzing <a.stintzing@metaways.de>
- * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2012-2014 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  */
 Ext.ns('Tine.widgets.relation');
@@ -254,28 +254,28 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
      * @return {String}
      */
     getViewRowClass: function(record, index, rowParams, store) {
-        if (this.invalidRowRecords && this.invalidRowRecords.indexOf(record.id) !== -1) {
-            
-            var model = record.get('related_model').split('_Model_');
-            model = Tine[model[0]].Model[model[1]];
+        var relatedModel = record.get('related_model').split('_Model_');
+            relatedModel = Tine[relatedModel[0]].Model[relatedModel[1]];
+        
+        var ownModel = record.get('own_model').split('_Model_');
+            ownModel = Tine[ownModel[0]].Model[ownModel[1]];
             
+        if (this.invalidRowRecords && this.invalidRowRecords.indexOf(record.id) !== -1) {
             rowParams.body = '<div style="height: 19px; margin-top: -19px" ext:qtip="' +
-                String.format(_('The maximum number of {0} with the type {1} is reached. Please change the type of this relation'), model.getRecordsName(), this.grid.typeRenderer(record.get('type'), null, record))
+                String.format(_("The maximum number of {0} with the type \"{1}\" is reached. Please change the type of this relation"), ownModel.getRecordsName(), this.grid.typeRenderer(record.get('type'), null, record))
                 + '"></div>';
             return 'tine-editorgrid-row-invalid';
         } else if (this.invalidRelatedRecords && this.invalidRelatedRecords.indexOf(record.id) !== -1) {
-            
-            var model = record.get('own_model').split('_Model_');
-            model = Tine[model[0]].Model[model[1]];
-            
             rowParams.body = '<div style="height: 19px; margin-top: -19px" ext:qtip="' +
-                String.format(_('The maximum number of {0}s with the type {1} is reached at the {2} you added. Please change the type of this relation or edit the {2}'), model.getRecordsName(), this.grid.typeRenderer(record.get('type'), null, record), model.getRecordName())
+                String.format(_("The maximum number of {0}s with the type \"{1}\" is reached at the {2} you added. Please change the type of this relation or edit the {2}"), ownModel.getRecordsName(), this.grid.typeRenderer(record.get('type'), null, record), relatedModel.getRecordName())
                 + '"></div>';
             return 'tine-editorgrid-row-invalid';
         }
+        
         rowParams.body='';
         return '';
     },
+    
     /**
      * calls the editdialog for the model
      */
@@ -747,7 +747,7 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
                 related_model: relatedPhpModel,
                 type: type,
                 own_degree: 'sibling'
-            }, relconf)), record.id);
+            }, relconf)), Ext.id());
             
             var mySideValid = true;
             
@@ -846,7 +846,7 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
         var invalid = false;
         
         Ext.each(constrainsConfig, function(conf) {
-            
+        
             if (conf.hasOwnProperty('max') && conf.max > 0 && (conf.type == relationRecord.get('type'))) {
                 var rr = record.get('relations'),
                     count = 0;
@@ -864,7 +864,8 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
                     if (! this.view.invalidRelatedRecords) {
                         this.view.invalidRelatedRecords = [];
                     }
-                    this.view.invalidRelatedRecords.push(relationRecord.get('related_id'));
+                    
+                    this.view.invalidRelatedRecords.push(relationRecord.id);
 
                     invalid = true;
                 }
@@ -949,12 +950,13 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
      * @param {Object} o
      */
     onValidateRowEdit: function(o) {
-        if(o.field === 'type') {
+        if (o.field === 'type') {
             
             var index = -1;
             
             this.store.remove(o.record.getId());
             this.removeFromInvalidRelatedRecords(o.record);
+            this.removeFromInvalidRowRecords(o.record);
             
             var model = o.record.get('related_model').split('_Model_');
             var app = model[0];
@@ -966,8 +968,6 @@ Tine.widgets.relation.GenericPickerGridPanel = Ext.extend(Tine.widgets.grid.Pick
                 this.checkLocalConstraints(app, model, o.record, o.value, o.originalValue);
             }
             
-            this.removeFromInvalidRelatedRecords(o.record);
-            
             // check constrains from other side
             this.validateRelatedConstrainsConfig(o.record.get('related_record'), o.record, true);
         }
index 5ee8685..51020ae 100644 (file)
@@ -1785,9 +1785,9 @@ msgstr "Kind"
 
 #: js/widgets/relation/GenericPickerGridPanel.js:263
 msgid ""
-"The maximum number of {0} with the type {1} is reached. Please change the "
+"The maximum number of {0} with the type \"{1}\" is reached. Please change the "
 "type of this relation"
-msgstr "Die maximale Anzahl von {0} mit Typ {1} wurde erreicht. Bitte ändern Sie den Typ der Verknüpfung."
+msgstr "Die maximale Anzahl an {0}n mit dem Typ \"{1}\" wurde erreicht. Bitte ändern Sie den Typ der Verknüpfung."
 
 #: js/widgets/relation/GenericPickerGridPanel.js:272
 msgid ""
@@ -2939,3 +2939,15 @@ msgstr "Monat falsch angegeben!"
 
 msgid "or"
 msgstr "oder"
+
+msgid "from"
+msgstr "von"
+
+msgid "to"
+msgstr "an"
+
+msgid "You tried to create a relation which is forbidden by the constraints config of one of the models."
+msgstr "Sie haben versucht, eine Verknüpfung zu aktualisieren, wodurch Verknüpfungen entstanden sind, die nicht erlaubt sind."
+
+msgid "Invalid Relations"
+msgstr "Fehlerhafte Verknüpfung"
index b5cac1d..64bb489 100644 (file)
@@ -25,6 +25,14 @@ msgstr ""
 msgid "Generic System Exception"
 msgstr ""
 
+#: Exception/MonthFormat.php:26
+msgid "Wrong month format!"
+msgstr ""
+
+#: Exception/MonthFormat.php:31
+msgid "The month must have the format YYYY-MM!"
+msgstr ""
+
 #: Exception/Record/SystemContainer.php:22
 msgid "System Container"
 msgstr ""
@@ -33,6 +41,16 @@ msgstr ""
 msgid "This is a system container which could not be deleted!"
 msgstr ""
 
+#: Exception/InvalidRelationConstraints.php:26
+msgid "Invalid Relations"
+msgstr ""
+
+#: Exception/InvalidRelationConstraints.php:36
+msgid ""
+"You tried to create a relation which is forbidden by the constraints config "
+"of one of the models."
+msgstr ""
+
 #: Acl/Rights.php:147
 msgid "Report bugs"
 msgstr ""
@@ -539,7 +557,7 @@ msgstr ""
 msgid "modified"
 msgstr ""
 
-#: Export/Spreadsheet/Ods.php:276
+#: Export/Spreadsheet/Ods.php:358
 msgid "Data"
 msgstr ""
 
@@ -805,7 +823,7 @@ msgid "Logging you in..."
 msgstr ""
 
 #: js/LoginPanel.js:460 js/widgets/tree/ContextMenu.js:191
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:683
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:693
 #: js/widgets/persistentfilter/PickerPanel.js:327
 #: js/widgets/persistentfilter/PickerPanel.js:365
 #: js/widgets/persistentfilter/PickerPanel.js:417
@@ -841,13 +859,13 @@ msgstr ""
 #: js/widgets/dialog/PreferencesDialog.js:248
 #: js/widgets/dialog/PreferencesDialog.js:277
 #: js/widgets/dialog/EditDialog.js:798 js/widgets/dialog/ExportDialog.js:150
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:645
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:655
 msgid "Errors"
 msgstr ""
 
 #: js/LoginPanel.js:508 js/widgets/dialog/CredentialsDialog.js:126
 #: js/widgets/dialog/EditDialog.js:820 js/widgets/dialog/ExportDialog.js:150
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:645
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:655
 msgid "Please fix the errors noted."
 msgstr ""
 
@@ -908,8 +926,8 @@ msgid "Your password has been changed."
 msgstr ""
 
 #: js/PasswordChangeDialog.js:100 js/PasswordChangeDialog.js:110
-#: js/widgets/relation/GenericPickerGridPanel.js:947
-#: js/widgets/relation/GenericPickerGridPanel.js:960
+#: js/widgets/relation/GenericPickerGridPanel.js:916
+#: js/widgets/relation/GenericPickerGridPanel.js:929
 #: js/widgets/dialog/ImportDialog.js:654
 #: js/widgets/dialog/MultipleEditResultSummary.js:196
 #: js/widgets/form/RecordPickerComboBox.js:294
@@ -968,9 +986,9 @@ msgstr ""
 msgid "Install Tine 2.0 as web app in your browser."
 msgstr ""
 
-#: js/MainMenu.js:243 js/widgets/grid/GridPanel.js:1761
+#: js/MainMenu.js:243 js/widgets/grid/GridPanel.js:1773
 #: js/widgets/tree/ContextMenu.js:346 js/widgets/dialog/EditDialog.js:827
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:680
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:690
 #: js/widgets/persistentfilter/PickerPanel.js:325
 msgid "Confirm"
 msgstr ""
@@ -1090,11 +1108,11 @@ msgid ""
 "Your user account has no role memberships. Please contact your administrator."
 msgstr ""
 
-#: js/ExceptionHandler.js:281
+#: js/ExceptionHandler.js:289
 msgid "Method Not Found / Insufficent Permissions"
 msgstr ""
 
-#: js/ExceptionHandler.js:282
+#: js/ExceptionHandler.js:290
 msgid ""
 "You tried to access a function that is not available. Please reload your "
 "browser, try again or contact your administrator."
@@ -1535,7 +1553,7 @@ msgstr ""
 msgid "Resume upload"
 msgstr ""
 
-#: js/widgets/grid/FileUploadGrid.js:241 js/widgets/grid/GridPanel.js:548
+#: js/widgets/grid/FileUploadGrid.js:241 js/widgets/grid/GridPanel.js:559
 #: js/widgets/tree/ContextMenu.js:35
 msgid "Add {0}"
 msgstr ""
@@ -1639,92 +1657,92 @@ msgid ""
 "view-options or change the module you search in."
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:312 js/widgets/grid/GridPanel.js:522
-#: js/widgets/grid/GridPanel.js:523 js/widgets/grid/GridPanel.js:524
+#: js/widgets/grid/GridPanel.js:312 js/widgets/grid/GridPanel.js:533
+#: js/widgets/grid/GridPanel.js:534 js/widgets/grid/GridPanel.js:535
 msgid "Edit {0}"
 msgid_plural "Edit {0}"
 msgstr[0] ""
 msgstr[1] ""
 
-#: js/widgets/grid/GridPanel.js:537 js/widgets/dialog/EditDialog.js:599
+#: js/widgets/grid/GridPanel.js:548 js/widgets/dialog/EditDialog.js:599
 msgid "Copy {0}"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:556
+#: js/widgets/grid/GridPanel.js:567
 msgid "Print Page"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:586
+#: js/widgets/grid/GridPanel.js:597
 #: js/widgets/dialog/DuplicateMergeDialog.js:220
 msgid "Merge {0}"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:622 js/widgets/grid/GridPanel.js:623
-#: js/widgets/grid/GridPanel.js:625 js/widgets/tree/ContextMenu.js:52
+#: js/widgets/grid/GridPanel.js:633 js/widgets/grid/GridPanel.js:634
+#: js/widgets/grid/GridPanel.js:636 js/widgets/tree/ContextMenu.js:52
 msgid "Delete {0}"
 msgid_plural "Delete {0}"
 msgstr[0] ""
 msgstr[1] ""
 
-#: js/widgets/grid/GridPanel.js:1038
+#: js/widgets/grid/GridPanel.js:1050
 msgid "No data to display"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1060
+#: js/widgets/grid/GridPanel.js:1072
 msgid "Displaying records {0} - {1} of {2}"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1061
+#: js/widgets/grid/GridPanel.js:1073
 msgid "No {0} to display"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1303
+#: js/widgets/grid/GridPanel.js:1315
 msgid "New..."
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1322
+#: js/widgets/grid/GridPanel.js:1334
 msgid "Add to..."
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1370
+#: js/widgets/grid/GridPanel.js:1382
 #: js/widgets/relation/GenericPickerGridPanel.js:461
 #: js/widgets/dialog/AttachmentsGridPanel.js:104
 msgid "Creation Time"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1371
+#: js/widgets/grid/GridPanel.js:1383
 #: js/widgets/dialog/AttachmentsGridPanel.js:106
 #: js/widgets/ActivitiesPanel.js:492
 msgid "Created By"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1372
+#: js/widgets/grid/GridPanel.js:1384
 msgid "Last Modified Time"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1373
+#: js/widgets/grid/GridPanel.js:1385
 msgid "Last Modified By"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1735
+#: js/widgets/grid/GridPanel.js:1747
 msgid "Not Allowed"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1736
+#: js/widgets/grid/GridPanel.js:1748
 msgid "You are not allowed to delete all pages at once"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1759
+#: js/widgets/grid/GridPanel.js:1771
 msgid "Do you really want to delete the selected record ({0})?"
 msgid_plural "Do you really want to delete the selected records ({0})?"
 msgstr[0] ""
 msgstr[1] ""
 
-#: js/widgets/grid/GridPanel.js:1804 js/widgets/dialog/EditDialog.js:829
+#: js/widgets/grid/GridPanel.js:1816 js/widgets/dialog/EditDialog.js:829
 msgid "Deleting {0}"
 msgstr ""
 
-#: js/widgets/grid/GridPanel.js:1804
+#: js/widgets/grid/GridPanel.js:1816
 msgid " ... This may take a long time!"
 msgstr ""
 
@@ -1771,15 +1789,15 @@ msgstr ""
 msgid "Child"
 msgstr ""
 
-#: js/widgets/relation/GenericPickerGridPanel.js:263
+#: js/widgets/relation/GenericPickerGridPanel.js:265
 msgid ""
-"The maximum number of {0} with the type {1} is reached. Please change the "
-"type of this relation"
+"The maximum number of {0} with the type \"{1}\" is reached. Please change "
+"the type of this relation"
 msgstr ""
 
-#: js/widgets/relation/GenericPickerGridPanel.js:272
+#: js/widgets/relation/GenericPickerGridPanel.js:270
 msgid ""
-"The maximum number of {0}s with the type {1} is reached at the {2} you "
+"The maximum number of {0}s with the type \"{1}\" is reached at the {2} you "
 "added. Please change the type of this relation or edit the {2}"
 msgstr ""
 
@@ -1805,13 +1823,13 @@ msgstr ""
 msgid "Dependency"
 msgstr ""
 
-#: js/widgets/relation/GenericPickerGridPanel.js:948
+#: js/widgets/relation/GenericPickerGridPanel.js:917
 msgid ""
 "The record you tried to link is already linked. Please edit the existing "
 "link."
 msgstr ""
 
-#: js/widgets/relation/GenericPickerGridPanel.js:961
+#: js/widgets/relation/GenericPickerGridPanel.js:930
 #: js/widgets/form/RecordPickerComboBox.js:295
 msgid "You tried to link a record with itself. This is not allowed!"
 msgstr ""
@@ -2291,33 +2309,33 @@ msgid "Select Export Definition ..."
 msgstr ""
 
 #: js/widgets/dialog/MultipleEditDialogPlugin.js:341
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:465
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:475
 msgid "Delete value from all selected records"
 msgstr ""
 
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:448
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:458
 msgid "Undo change for all selected records"
 msgstr ""
 
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:523
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:533
 msgid "Edit {0} {1}"
 msgstr ""
 
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:582
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:592
 msgid "Different Values"
 msgstr ""
 
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:583
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:593
 msgid ""
 "This field has different values. Editing this field will overwrite the old "
 "values."
 msgstr ""
 
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:680
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:690
 msgid "Do you really want to change these {0} records?"
 msgstr ""
 
-#: js/widgets/dialog/MultipleEditDialogPlugin.js:683
+#: js/widgets/dialog/MultipleEditDialogPlugin.js:693
 msgid "Applying changes"
 msgstr ""
 
@@ -2917,12 +2935,3 @@ msgid ""
 "You are not allowed to delete this Container. Please define another "
 "container as the default addressbook for internal contacts!"
 msgstr ""
-
-msgid "or"
-msgstr ""
-
-msgid "The month must have the format YYYY-MM!"
-msgstr ""
-
-msgid "Wrong month format!"
-msgstr ""