#5024: allow to attach external files to records
authorPhilipp Schüle <p.schuele@metaways.de>
Thu, 4 Jul 2013 07:44:33 +0000 (09:44 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Thu, 4 Jul 2013 08:12:00 +0000 (10:12 +0200)
- add first version of attachment grid to generic edit dialog
- allow to attach new tempfiles to record
- allow to remove attachments from record
- allow to download file attachments
- return all record attachments when fetching full record
- delete attachments when record is deleted
- supported models: contact, task, lead, event
- improved generic foreign records resolving

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

Change-Id: I04f36a657a922e5f87124ec634510476f59ec321
Reviewed-on: https://gerrit.tine20.org/tine20/2155
Tested-by: jenkins user
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
39 files changed:
tests/tine20/Calendar/Convert/Event/VCalendar/GenericTest.php
tests/tine20/Calendar/JsonTests.php
tests/tine20/Crm/Export/CsvTest.php
tests/tine20/Crm/JsonTest.php
tests/tine20/Filemanager/Frontend/JsonTests.php
tests/tine20/Tasks/JsonTest.php
tine20/Addressbook/Model/Contact.php
tine20/Addressbook/js/Model.js
tine20/Calendar/Controller/Event.php
tine20/Calendar/Controller/MSEventFacade.php
tine20/Calendar/Convert/Event/Json.php
tine20/Calendar/Model/Event.php
tine20/Calendar/js/Model.js
tine20/Courses/Model/Course.php
tine20/Crm/Model/Lead.php
tine20/Crm/js/LeadEditDialog.js
tine20/Crm/js/Model.js
tine20/Felamimail/js/MessageEditDialog.js
tine20/Filemanager/Controller/Node.php
tine20/Filemanager/Frontend/Http.php
tine20/Tasks/Frontend/Json.php
tine20/Tasks/Model/Task.php
tine20/Tasks/js/Models.js
tine20/Tinebase/Controller/Record/Abstract.php
tine20/Tinebase/Convert/Json.php
tine20/Tinebase/FileSystem.php
tine20/Tinebase/FileSystem/RecordAttachments.php [new file with mode: 0644]
tine20/Tinebase/Frontend/Http.php
tine20/Tinebase/Frontend/Http/Abstract.php
tine20/Tinebase/Frontend/Json/Abstract.php
tine20/Tinebase/Model/Tree/Node.php
tine20/Tinebase/Record/RecordSet.php
tine20/Tinebase/Tinebase.jsb2
tine20/Tinebase/User/Sql.php
tine20/Tinebase/css/Tinebase.css
tine20/Tinebase/js/Models.js
tine20/Tinebase/js/widgets/dialog/AttachmentsGridPanel.js [new file with mode: 0644]
tine20/Tinebase/js/widgets/dialog/EditDialog.js
tine20/Tinebase/js/widgets/grid/FileUploadGrid.js

index 7360b1c..45e362f 100644 (file)
@@ -385,7 +385,7 @@ class Calendar_Convert_Event_VCalendar_GenericTest extends PHPUnit_Framework_Tes
         $event = $this->testConvertToTine20Model();
         $event->creation_time      = new Tinebase_DateTime('2011-11-11 11:11', 'UTC');
         $event->last_modified_time = new Tinebase_DateTime('2011-11-11 12:12', 'UTC');
-        $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attendee');
+        $event->attendee = new Tinebase_Record_RecordSet('Calendar_Model_Attender');
         
         $converter = Calendar_Convert_Event_VCalendar_Factory::factory(Calendar_Convert_Event_VCalendar_Factory::CLIENT_GENERIC);
         
index 650f7b5..6a30ae7 100644 (file)
@@ -549,18 +549,7 @@ class Calendar_JsonTests extends Calendar_TestCase
         $someRecurInstance['seq'] = 2;
         $this->_uit->updateRecurSeries($someRecurInstance, FALSE, FALSE);
         
-        $from = $recurSet[0]['dtstart'];
-        $until = new Tinebase_DateTime($from);
-        $until->addWeek(5)->addHour(10);
-        $until = $until->get(Tinebase_Record_Abstract::ISO8601LONG);
-        
-        $filter = array(
-            array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_testCalendar->getId()),
-            array('field' => 'period',       'operator' => 'within', 'value' => array('from' => $from, 'until' => $until)),
-        );
-        
-        $searchResultData = $this->_uit->searchEvents($filter, array());
-        
+        $searchResultData = $this->_searchRecurSeries($recurSet[0]);
         $this->assertEquals(6, count($searchResultData['results']));
         
         $summaryMap = array();
@@ -579,6 +568,27 @@ class Calendar_JsonTests extends Calendar_TestCase
     }
     
     /**
+     * search updated recur set
+     * 
+     * @param array $firstInstance
+     * @return array
+     */
+    protected function _searchRecurSeries($firstInstance)
+    {
+        $from = $firstInstance['dtstart'];
+        $until = new Tinebase_DateTime($from);
+        $until->addWeek(5)->addHour(10);
+        $until = $until->get(Tinebase_Record_Abstract::ISO8601LONG);
+        
+        $filter = array(
+            array('field' => 'container_id', 'operator' => 'equals', 'value' => $this->_testCalendar->getId()),
+            array('field' => 'period',       'operator' => 'within', 'value' => array('from' => $from, 'until' => $until)),
+        );
+        
+        return $this->_uit->searchEvents($filter, array());
+    }
+    
+    /**
      * testUpdateRecurExceptionsFromSeriesOverDstMove
      * 
      * @todo implement
@@ -1300,4 +1310,30 @@ class Calendar_JsonTests extends Calendar_TestCase
             $this->assertEquals(Calendar_Model_Attender::STATUS_TENTATIVE, $attender['status'], 'both attendee status should be TENTATIVE: ' . print_r($attender, TRUE));
         }
     }
+    
+    /**
+     * testAddAttachmentToRecurSeries
+     * 
+     * @see 0005024: allow to attach external files to records
+     */
+    public function testAddAttachmentToRecurSeries()
+    {
+        $tempFileBackend = new Tinebase_TempFile();
+        $tempFile = $tempFileBackend->createTempFile(dirname(dirname(__FILE__)) . '/Filemanager/files/test.txt');
+        
+        $recurSet = array_value('results', $this->testSearchRecuringIncludes());
+        // update recurseries 
+        $someRecurInstance = $recurSet[2];
+        $someRecurInstance['attachments'] = array(array('tempFile' => array('id' => $tempFile->getId())));
+        $someRecurInstance['seq'] = 2;
+        $this->_uit->updateRecurSeries($someRecurInstance, FALSE, FALSE);
+        
+        $searchResultData = $this->_searchRecurSeries($recurSet[0]);
+        foreach ($searchResultData['results'] as $recurInstance) {
+            $this->assertTrue(isset($recurInstance['attachments']), 'no attachments found in event: ' . print_r($recurInstance, TRUE));
+            $this->assertEquals(1, count($recurInstance['attachments']));
+            $attachment = $recurInstance['attachments'][0];
+            $this->assertEquals('text/plain', $attachment['contenttype'], print_r($attachment, TRUE));
+        }
+    }
 }
index d41e923..d9afbc9 100644 (file)
@@ -87,10 +87,10 @@ class Crm_Export_CsvTest extends Crm_Export_AbstractTest
         
         $defaultContainerId = Tinebase_Container::getInstance()->getDefaultContainer('Crm')->getId();
         $this->assertContains('"lead_name","leadstate_id","Leadstate","leadtype_id","Leadtype","leadsource_id","Leadsource","container_id","start"'
-            . ',"description","end","turnover","probableTurnover","probability","end_scheduled","tags","notes","seq","tags",'
+            . ',"description","end","turnover","probableTurnover","probability","end_scheduled","tags","attachments","notes","seq","tags",'
             . '"CUSTOMER-org_name","CUSTOMER-n_family","CUSTOMER-n_given","CUSTOMER-adr_one_street","CUSTOMER-adr_one_postalcode","CUSTOMER-adr_one_locality",'
             . '"CUSTOMER-adr_one_countryname","CUSTOMER-tel_work","CUSTOMER-tel_cell","CUSTOMER-email",'
-            . '"PARTNER","RESPONSIBLE","TASK"', $export, 'headline wrong');
+            . '"PARTNER","RESPONSIBLE","TASK"', $export, 'headline wrong: ' . substr($export, 0, 360));
         $this->assertContains('"PHPUnit","1","' . $translate->_('open') . '","1","' . $translate->_('Customer') . '","1","' . $translate->_('Market') . '","' 
             . $defaultContainerId . '"', $export, 'data #1 wrong');
         $this->assertContains('"Metaways Infosystems GmbH","Kneschke","Lars","Pickhuben 4","24xxx","Hamburg","DE","+49TELWORK","+49TELCELL","unittests@tine20.org"'
index ff8b5a3..8cfd55c 100644 (file)
@@ -4,7 +4,7 @@
  * 
  * @package     Crm
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2008-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2008-2013 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Philipp Schüle <p.schuele@metaways.de>
  * 
  */
@@ -20,6 +20,11 @@ require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'TestHelper.php'
 class Crm_JsonTest extends Crm_AbstractTest
 {
     /**
+     * @var array test objects
+     */
+    protected $_objects = array();
+    
+    /**
      * Backend
      *
      * @var Crm_Frontend_Json
@@ -27,6 +32,13 @@ class Crm_JsonTest extends Crm_AbstractTest
     protected $_instance;
     
     /**
+     * fs controller
+     *
+     * @var Tinebase_FileSystem
+     */
+    protected $_fsController;
+    
+    /**
      * Runs the test methods of this class.
      *
      * @access public
@@ -47,7 +59,10 @@ class Crm_JsonTest extends Crm_AbstractTest
     protected function setUp()
     {
         Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
+
         $this->_instance = new Crm_Frontend_Json();
+        $this->_fsController = Tinebase_FileSystem::getInstance();
+        
         Addressbook_Controller_Contact::getInstance()->setGeoDataForContacts(FALSE);
     }
 
@@ -59,6 +74,16 @@ class Crm_JsonTest extends Crm_AbstractTest
      */
     protected function tearDown()
     {
+        if (isset($this->_objects['paths'])) {
+            foreach ($this->_objects['paths'] as $path) {
+                try {
+                    $this->_fsController->rmdir($path, TRUE);
+                } catch (Tinebase_Exception_NotFound $tenf) {
+                    // already deleted
+                }
+            }
+        }
+        
         Addressbook_Controller_Contact::getInstance()->setGeoDataForContacts(TRUE);
         Tinebase_TransactionManager::getInstance()->rollBack();
     }
@@ -544,4 +569,72 @@ class Crm_JsonTest extends Crm_AbstractTest
             }
         }
     }
+    
+    /**
+     * testCreateLeadWithAttachment
+     * 
+     * @see 0005024: allow to attach external files to records
+     */
+    public function testCreateLeadWithAttachment()
+    {
+        $tempFileBackend = new Tinebase_TempFile();
+        $tempFile = $tempFileBackend->createTempFile(dirname(dirname(__FILE__)) . '/Filemanager/files/test.txt');
+        
+        $lead = $this->_getLead()->toArray();
+        $lead['attachments'] = array(array('tempFile' => array('id' => $tempFile->getId())));
+        
+        $savedLead = $this->_instance->saveLead($lead);
+        // add path to files to remove
+        $this->_objects['paths'][] = Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachmentPath(new Crm_Model_Lead($savedLead, TRUE)) . '/' . $tempFile->name;
+        
+        $this->assertTrue(isset($savedLead['attachments']), 'no attachments found');
+        $this->assertEquals(1, count($savedLead['attachments']));
+        $attachment = $savedLead['attachments'][0];
+        $this->assertEquals('text/plain', $attachment['contenttype'], print_r($attachment, TRUE));
+        $this->assertEquals(17, $attachment['size']);
+        $this->assertTrue(is_array($attachment['created_by']), 'user not resolved: ' . print_r($attachment['created_by'], TRUE));
+        $this->assertEquals(Tinebase_Core::getUser()->accountFullName, $attachment['created_by']['accountFullName'], 'user not resolved: ' . print_r($attachment['created_by'], TRUE));
+        
+        return $savedLead;
+    }
+    
+    /**
+     * testUpdateLeadWithAttachment
+     * 
+     * @see 0005024: allow to attach external files to records
+     */
+    public function testUpdateLeadWithAttachment()
+    {
+        $lead = $this->testCreateLeadWithAttachment();
+        $savedLead = $this->_instance->saveLead($lead);
+        $this->assertTrue(isset($savedLead['attachments']), 'no attachments found');
+        $this->assertEquals(1, count($savedLead['attachments']));
+    }
+    
+    /**
+     * testRemoveAttachmentFromLead
+     * 
+     * @see 0005024: allow to attach external files to records
+     */
+    public function testRemoveAttachmentFromLead()
+    {
+        $lead = $this->testCreateLeadWithAttachment();
+        $lead['attachments'] = array();
+    
+        $savedLead = $this->_instance->saveLead($lead);
+        $this->assertEquals(0, count($savedLead['attachments']));
+        $this->assertFalse($this->_fsController->fileExists($this->_objects['paths'][0]));
+    }
+    
+    /**
+     * testDeleteLeadWithAttachment
+     * 
+     * @see 0005024: allow to attach external files to records
+     */
+    public function testDeleteLeadWithAttachment()
+    {
+        $lead = $this->testCreateLeadWithAttachment();
+        $this->_instance->deleteLeads(array($lead['id']));
+        $this->assertFalse($this->_fsController->fileExists($this->_objects['paths'][0]));
+    }
 }
index 3c1224f..de14c04 100644 (file)
@@ -34,7 +34,7 @@ class Filemanager_Frontend_JsonTests extends PHPUnit_Framework_TestCase
     protected $_json;
     
     /**
-     * uit
+     * fs controller
      *
      * @var Tinebase_FileSystem
      */
index 09263ad..235fc08 100644 (file)
@@ -5,8 +5,8 @@
  * @package     Tinebase
  * @subpackage  Record
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
- * @copyright   Copyright (c) 2007-2010 Metaways Infosystems GmbH (http://www.metaways.de)
- * @author      Philipp Schuele <p.schuele@metaways.de>
+ * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Philipp Schüle <p.schuele@metaways.de>
  */
 
 /**
@@ -65,6 +65,8 @@ class Tasks_JsonTest extends PHPUnit_Framework_TestCase
      */
     protected function setUp()
     {
+        Tinebase_TransactionManager::getInstance()->startTransaction(Tinebase_Core::getDb());
+        
         $this->_backend = new Tasks_Frontend_Json();
         $this->_smtpConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::SMTP, new Tinebase_Config_Struct())->toArray();
         $this->_smtpTransport = Tinebase_Smtp::getDefaultTransport();
@@ -78,12 +80,12 @@ class Tasks_JsonTest extends PHPUnit_Framework_TestCase
      */
     protected function tearDown()
     {
-        parent::tearDown();
-        
         if ($this->_smtpConfigChanged) {
             Tinebase_Config::getInstance()->set(Tinebase_Config::SMTP, $this->_smtpConfig);
             Tinebase_Smtp::setDefaultTransport($this->_smtpTransport);
         }
+        
+        Tinebase_TransactionManager::getInstance()->rollBack();
     }
     
     /**
@@ -346,7 +348,7 @@ class Tasks_JsonTest extends PHPUnit_Framework_TestCase
         $task->organizer = $organizer;
         $returned = $this->_backend->saveTask($task->toArray());
         $taskId = $returned['id'];
-               
+        
         // check search tasks- organizer exists
         $tasks = $this->_backend->searchTasks($this->_getFilter(), $this->_getPaging());
         $this->assertEquals(1, $tasks['totalcount'], 'more (or less) than one tasks found');
@@ -356,21 +358,20 @@ class Tasks_JsonTest extends PHPUnit_Framework_TestCase
         $task = $this->_backend->getTask($taskId);
         $this->assertEquals($task['organizer']['accountId'], $organizerId);
 
-        // delete user
         Tinebase_User::getInstance()->deleteUser($organizerId);
 
         // test seach search tasks - organizer is deleted
         $tasks = $this->_backend->searchTasks($this->_getFilter(), $this->_getPaging());
         $this->assertEquals(1, $tasks['totalcount'], 'more (or less) than one tasks found');
 
-        $this->assertEquals($tasks['results'][0]['organizer']['accountDisplayName'], Tinebase_User::getInstance()->getNonExistentUser()->accountDisplayName);
+        $organizer = $tasks['results'][0]['organizer'];
+        $this->assertTrue(is_array($organizer), 'organizer not resolved: ' . print_r($tasks['results'][0], TRUE));
+        $this->assertEquals($organizer['accountDisplayName'], Tinebase_User::getInstance()->getNonExistentUser()->accountDisplayName,
+            'accountDisplayName not found in organizer: ' . print_r($organizer, TRUE));
 
         // test get single task - organizer is deleted
         $task = $this->_backend->getTask($taskId);
         $this->assertEquals($task['organizer']['accountDisplayName'], Tinebase_User::getInstance()->getNonExistentUser()->accountDisplayName);
-        
-        //Cleanup test objects
-        $this->_backend->deleteTasks(array($taskId));
     }
     
     /**
@@ -380,17 +381,20 @@ class Tasks_JsonTest extends PHPUnit_Framework_TestCase
      */
     protected function _createUser()
     {
-        $user = new Tinebase_Model_FullUser(array(
-//            'accountId'             => 100,
-            'accountLoginName'      => 'creator',
-            'accountStatus'         => 'enabled',
-            'accountExpires'        => NULL,
-            'accountPrimaryGroup'   => Tinebase_Group::getInstance()->getGroupByName('Users')->id,
-            'accountLastName'       => 'Tine 2.0',
-            'accountFirstName'      => 'Creator',
-            'accountEmailAddress'   => 'phpunit@metaways.de'
-        ));
-        Tinebase_User::getInstance()->addUser($user);
+        try {
+            $user = Tinebase_User::getInstance()->getUserByLoginName('creator');
+        } catch (Tinebase_Exception_NotFound $tenf) {
+            $user = new Tinebase_Model_FullUser(array(
+                'accountLoginName'      => 'creator',
+                'accountStatus'         => 'enabled',
+                'accountExpires'        => NULL,
+                'accountPrimaryGroup'   => Tinebase_Group::getInstance()->getGroupByName('Users')->id,
+                'accountLastName'       => 'Tine 2.0',
+                'accountFirstName'      => 'Creator',
+                'accountEmailAddress'   => 'phpunit@metaways.de'
+            ));
+            $user = Tinebase_User::getInstance()->addUser($user);
+        }
         
         return $user;
     }
index 32c507c..92d1756 100644 (file)
@@ -107,7 +107,8 @@ class Addressbook_Model_Contact extends Tinebase_Record_Abstract
      * @var array
      */
     protected static $_resolveForeignIdFields = array(
-        'Tinebase_Model_User' => array('created_by', 'last_modified_by')
+        'Tinebase_Model_User' => array('created_by', 'last_modified_by'),
+        'recursive'           => array('attachments' => 'Tinebase_Model_Tree_Node'),
     );
     
     /**
@@ -203,6 +204,7 @@ class Addressbook_Model_Contact extends Tinebase_Record_Abstract
         'tags'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'notes'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'relations'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        'attachments'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'customfields'          => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => array()),
         'type'                  => array(
             Zend_Filter_Input::ALLOW_EMPTY => true,
index 0658819..e74314a 100644 (file)
@@ -73,6 +73,7 @@ Tine.Addressbook.Model.ContactArray = Tine.Tinebase.Model.genericFields.concat([
     {name: 'tags'},
     {name: 'notes', omitDuplicateResolving: true},
     {name: 'relations', omitDuplicateResolving: true},
+    {name: 'attachments', omitDuplicateResolving: true},
     {name: 'customfields', isMetaField: true},
     {name: 'type', omitDuplicateResolving: true}
 ]);
index 5821f9e..63e078b 100644 (file)
@@ -1058,6 +1058,23 @@ class Calendar_Controller_Event extends Tinebase_Controller_Record_Abstract impl
     }
     
     /**
+     * get by id
+     *
+     * @param string $_id
+     * @param int $_containerId
+     * @return Tinebase_Record_Interface
+     */
+    public function get($_id, $_containerId = NULL)
+    {
+        if (preg_match('/^fakeid/', $_id)) {
+            // get base event when trying to fetch a non-persistent recur instance
+            return $this->getRecurBaseEvent(new Calendar_Model_Event(array('uid' => substr(str_replace('fakeid', '', $_id), 0, 40))), TRUE);
+        } else {
+            return parent::get($_id, $_containerId);
+        }
+    }
+    
+    /**
      * returns base event of a recurring series
      *
      * @param  Calendar_Model_Event $_event
index 1c07200..090d5e7 100644 (file)
@@ -27,7 +27,7 @@
  * 
  * In iTIP Event handling is based on the perspective of a certain user. This user is the 
  * current user per default, but can be switched with
- * Calendar_Controller_MSEventFacade::setCalendarUser(Calendar_Model_Attendee $_calUser)
+ * Calendar_Controller_MSEventFacade::setCalendarUser(Calendar_Model_Attender $_calUser)
  * 
  * @package     Calendar
  * @subpackage  Controller
index 0d6be5d..31eafcf 100644 (file)
@@ -68,25 +68,13 @@ class Calendar_Convert_Event_Json extends Tinebase_Convert_Json
     */
     static public function resolveOrganizer($_events)
     {
-        $events = $_events instanceof Tinebase_Record_RecordSet ? $_events : array($_events);
-    
-        $organizerIds = array();
-        foreach ($events as $event) {
-            if ($event->organizer) {
-                $organizerIds[] = $event->organizer;
-            }
-        }
-    
-        $organizers = Addressbook_Controller_Contact::getInstance()->getMultiple(array_unique($organizerIds), TRUE);
-    
-        foreach ($events as $event) {
-            if ($event->organizer && is_scalar($event->organizer)) {
-                $idx = $organizers->getIndexById($event->organizer);
-                if ($idx !== FALSE) {
-                    $event->organizer = $organizers[$idx];
-                }
-            }
-        }
+        $events = $_events instanceof Tinebase_Record_RecordSet ? $_events : new Tinebase_Record_RecordSet('Calendar_Model_Event', array($_events));
+        self::resolveMultipleIdFields($events, array(
+            'Addressbook_Model_Contact' => array(
+                'options' => array('ignoreAcl' => TRUE),
+                'fields'  => array('organizer'),
+            )
+        ));
     }
     
     /**
@@ -106,12 +94,20 @@ class Calendar_Convert_Event_Json extends Tinebase_Convert_Json
 
         Tinebase_Notes::getInstance()->getMultipleNotesOfRecords($_records);
         Tinebase_Tags::getInstance()->getMultipleTagsOfRecords($_records);
+        Tinebase_FileSystem_RecordAttachments::getInstance()->getMultipleAttachmentsOfRecords($_records);
         
         Calendar_Model_Attender::resolveAttendee($_records->attendee, TRUE, $_records);
-        Calendar_Convert_Event_Json::resolveOrganizer($_records);
         Calendar_Convert_Event_Json::resolveRrule($_records);
         Calendar_Controller_Event::getInstance()->getAlarms($_records);
         
+        self::resolveMultipleIdFields($_records, array(
+            'Addressbook_Model_Contact' => array(
+                'options' => array('ignoreAcl' => TRUE),
+                'fields'  => array('organizer'),
+            ),
+            'recursive' => array('attachments' => 'Tinebase_Model_Tree_Node')
+        ));
+        
         Calendar_Model_Rrule::mergeAndRemoveNonMatchingRecurrences($_records, $_filter);
         $_records->sortByPagination($_pagination);
         
@@ -125,8 +121,8 @@ class Calendar_Convert_Event_Json extends Tinebase_Convert_Json
         foreach ($eventsData as $idx => $eventData) {
             if (! array_key_exists(Tinebase_Model_Grants::GRANT_READ, $eventData) || ! $eventData[Tinebase_Model_Grants::GRANT_READ]) {
                 $eventsData[$idx] = array_intersect_key($eventsData[$idx], array_flip(array(
-                    'id', 
-                    'dtstart', 
+                    'id',
+                    'dtstart',
                     'dtend',
                     'transp',
                     'is_all_day_event',
index af09d4e..36a7711 100644 (file)
@@ -72,70 +72,72 @@ class Calendar_Model_Event extends Tinebase_Record_Abstract
      */
     protected $_validators = array(
         // tine record fields
-        'id'                   => array('allowEmpty' => true,  /*'Alnum'*/),
-        'container_id'         => array('allowEmpty' => true,  'Int'  ),
-        'created_by'           => array('allowEmpty' => true,         ),
-        'creation_time'        => array('allowEmpty' => true          ),
-        'last_modified_by'     => array('allowEmpty' => true          ),
-        'last_modified_time'   => array('allowEmpty' => true          ),
-        'is_deleted'           => array('allowEmpty' => true          ),
-        'deleted_time'         => array('allowEmpty' => true          ),
-        'deleted_by'           => array('allowEmpty' => true          ),
-        'seq'                  => array('allowEmpty' => true,  'Int'  ),
+        'id'                   => array(Zend_Filter_Input::ALLOW_EMPTY => true,  /*'Alnum'*/),
+        'container_id'         => array(Zend_Filter_Input::ALLOW_EMPTY => true,  'Int'  ),
+        'created_by'           => array(Zend_Filter_Input::ALLOW_EMPTY => true,         ),
+        'creation_time'        => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'last_modified_by'     => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'last_modified_time'   => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'is_deleted'           => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'deleted_time'         => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'deleted_by'           => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'seq'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true,  'Int'  ),
         // calendar only fields
-        'dtend'                => array('allowEmpty' => true          ),
+        'dtend'                => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
         'transp'               => array(
-            'allowEmpty' => true,
+            Zend_Filter_Input::ALLOW_EMPTY => true,
             array('InArray', array(self::TRANSP_OPAQUE, self::TRANSP_TRANSP))
         ),
         // ical common fields
         'class'                => array(
-            'allowEmpty' => true,
+            Zend_Filter_Input::ALLOW_EMPTY => true,
             array('InArray', array(self::CLASS_PUBLIC, self::CLASS_PRIVATE, /*self::CLASS_CONFIDENTIAL*/))
         ),
-        'description'          => array('allowEmpty' => true          ),
-        'geo'                  => array('allowEmpty' => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
-        'location'             => array('allowEmpty' => true          ),
-        'organizer'            => array('allowEmpty' => true,         ),
-        'priority'             => array('allowEmpty' => true, 'Int'   ),
+        'description'          => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'geo'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
+        'location'             => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'organizer'            => array(Zend_Filter_Input::ALLOW_EMPTY => true,         ),
+        'priority'             => array(Zend_Filter_Input::ALLOW_EMPTY => true, 'Int'   ),
         'status'            => array(
-            'allowEmpty' => true,
+            Zend_Filter_Input::ALLOW_EMPTY => true,
             array('InArray', array(self::STATUS_CONFIRMED, self::STATUS_TENTATIVE, self::STATUS_CANCELED))
         ),
-        'summary'              => array('allowEmpty' => true          ),
-        'url'                  => array('allowEmpty' => true          ),
-        'uid'                  => array('allowEmpty' => true          ),
+        'summary'              => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'url'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
+        'uid'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true          ),
         // ical common fields with multiple appearance
-        //'attach'                => array('allowEmpty' => true         ),
-        'attendee'              => array('allowEmpty' => true         ), // RecordSet of Calendar_Model_Attender
-        'alarms'                => array('allowEmpty' => true         ), // RecordSet of Tinebase_Model_Alarm
-        'tags'                  => array('allowEmpty' => true         ), // originally categories handled by Tinebase_Tags
-        'notes'                 => array('allowEmpty' => true         ), // originally comment handled by Tinebase_Notes
-        //'contact'               => array('allowEmpty' => true         ),
-        //'related'               => array('allowEmpty' => true         ),
-        //'resources'             => array('allowEmpty' => true         ),
-        //'rstatus'               => array('allowEmpty' => true         ),
+        //'attach'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'attendee'              => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), // RecordSet of Calendar_Model_Attender
+        'alarms'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), // RecordSet of Tinebase_Model_Alarm
+        'tags'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), // originally categories handled by Tinebase_Tags
+        'notes'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), // originally comment handled by Tinebase_Notes
+        'attachments'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        
+        //'contact'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        //'related'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        //'resources'             => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        //'rstatus'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         // ical scheduleable interface fields
-        'dtstart'               => array('allowEmpty' => true         ),
-        'recurid'               => array('allowEmpty' => true         ),
+        'dtstart'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'recurid'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         // ical scheduleable interface fields with multiple appearance
-        'exdate'                => array('allowEmpty' => true         ), //  array of Tinebase_DateTimeTinebase_DateTime's
-        //'exrule'                => array('allowEmpty' => true         ),
-        //'rdate'                 => array('allowEmpty' => true         ),
-        'rrule'                 => array('allowEmpty' => true         ),
+        'exdate'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ), //  array of Tinebase_DateTimeTinebase_DateTime's
+        //'exrule'                => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        //'rdate'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'rrule'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         // calendar helper fields
-        'is_all_day_event'      => array('allowEmpty' => true         ),
-        'rrule_until'           => array('allowEmpty' => true         ),
-        'originator_tz'         => array('allowEmpty' => true         ),
+        'is_all_day_event'      => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'rrule_until'           => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'originator_tz'         => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
     
         // grant helper fields
-        Tinebase_Model_Grants::GRANT_FREEBUSY  => array('allowEmpty' => true),
-        Tinebase_Model_Grants::GRANT_READ    => array('allowEmpty' => true),
-        Tinebase_Model_Grants::GRANT_SYNC    => array('allowEmpty' => true),
-        Tinebase_Model_Grants::GRANT_EXPORT  => array('allowEmpty' => true),
-        Tinebase_Model_Grants::GRANT_EDIT    => array('allowEmpty' => true),
-        Tinebase_Model_Grants::GRANT_DELETE  => array('allowEmpty' => true),
-        Tinebase_Model_Grants::GRANT_PRIVATE => array('allowEmpty' => true),
+        Tinebase_Model_Grants::GRANT_FREEBUSY => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        Tinebase_Model_Grants::GRANT_READ     => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        Tinebase_Model_Grants::GRANT_SYNC     => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        Tinebase_Model_Grants::GRANT_EXPORT   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        Tinebase_Model_Grants::GRANT_EDIT     => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        Tinebase_Model_Grants::GRANT_DELETE   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        Tinebase_Model_Grants::GRANT_PRIVATE  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
     );
     
     /**
@@ -559,7 +561,7 @@ class Calendar_Model_Event extends Tinebase_Record_Abstract
     /**
      * checks if given attendee is organizer of this event
      * 
-     * @param Calendar_Model_Attendee $_attendee
+     * @param Calendar_Model_Attender $_attendee
      */
     public function isOrganizer($_attendee=NULL)
     {
index e124a7d..ce96726 100644 (file)
@@ -35,6 +35,7 @@ Tine.Calendar.Model.Event = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model
     { name: 'alarms'},
     { name: 'tags' },
     { name: 'notes'},
+    { name: 'attachments'},
     //{ name: 'contact' },
     //{ name: 'related' },
     //{ name: 'resources' },
index 2523861..3bb7a15 100644 (file)
@@ -58,7 +58,7 @@ class Courses_Model_Course extends Tinebase_Record_Abstract
         'seq'                   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
     // relations (linked Courses_Model_Course records) and other metadata
         'relations'             => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
-        'tags'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),    
+        'tags'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'notes'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
     );
 
index a6b2930..be95531 100644 (file)
@@ -6,7 +6,7 @@
  * @subpackage  Model
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Thomas Wadewitz <t.wadewitz@metaways.de>
- * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
  */
 
 /**
@@ -42,7 +42,8 @@ class Crm_Model_Lead extends Tinebase_Record_Abstract
      * @var array
      */
     protected static $_resolveForeignIdFields = array(
-        'Tinebase_Model_User' => array('created_by', 'last_modified_by')
+        'Tinebase_Model_User'     => array('created_by', 'last_modified_by'),
+        'recursive'               => array('attachments' => 'Tinebase_Model_Tree_Node'),
     );
     
     /**
@@ -81,6 +82,7 @@ class Crm_Model_Lead extends Tinebase_Record_Abstract
     // linked objects
         'tags'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'relations'             => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
+        'attachments'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'notes'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
         'customfields'          => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => array()),
     // modlog information
index f11d8b0..90a336e 100644 (file)
@@ -219,7 +219,7 @@ Tine.Crm.LeadEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
         this.combo_probability = new Ext.ux.PercentCombo({
             fieldLabel: this.app.i18n._('Probability'), 
             id: 'combo_probability',
-            anchor:'95%',            
+            anchor:'95%',
             name:'probability'
         });
         
@@ -462,11 +462,10 @@ Tine.Crm.LeadEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
                         ]} // end of accordion panel (east)
                     ] // end of lead tabpanel items
             }, new Tine.widgets.activities.ActivitiesTabPanel({
-                    app: this.appName,
-                    record_id: this.record.id,
-                    record_model: this.appName + '_Model_' + this.recordClass.getMeta('modelName')
-               }) // end of activities tabpanel
-            ] // end of main tabpanel items
+                app: this.appName,
+                record_id: this.record.id,
+                record_model: this.appName + '_Model_' + this.recordClass.getMeta('modelName')
+            })] // end of main tabpanel items
         }; // end of return
     } // end of getFormItems
 });
index a65c6ac..9a03341 100644 (file)
@@ -40,6 +40,7 @@ Tine.Crm.Model.Lead = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model.gener
         {name: 'products'},
         {name: 'tags'},
         {name: 'notes'},
+        {name: 'attachments'},
         {name: 'customfields', isMetaField: true}
     ]), {
     appName: 'Crm',
index befd6fc..042a556 100644 (file)
@@ -118,6 +118,7 @@ Tine.Felamimail.MessageEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
     recordProxy: Tine.Felamimail.messageBackend,
     loadRecord: false,
     evalGrants: false,
+    hideAttachmentsPanel: true,
     
     bodyStyle:'padding:0px',
     
@@ -784,7 +785,6 @@ Tine.Felamimail.MessageEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
      */
     initAttachmentGrid: function() {
         if (! this.attachmentGrid) {
-        
             this.attachmentGrid = new Tine.widgets.grid.FileUploadGrid({
                 fieldLabel: this.app.i18n._('Attachments'),
                 hideLabel: true,
index b6cbee7..d5d7728 100644 (file)
@@ -418,7 +418,7 @@ class Filemanager_Controller_Node extends Tinebase_Controller_Record_Abstract
         $path = (strpos($_path, '/') === 0) ? $_path : '/' . $_path;
         // only add base path once
         $result = (! preg_match('@^' . preg_quote($basePath) . '@', $path)) ? $basePath . $path : $path;
-                
+        
         return $result;
     }
     
@@ -617,14 +617,7 @@ class Filemanager_Controller_Node extends Tinebase_Controller_Record_Abstract
         
         switch ($_type) {
             case Tinebase_Model_Tree_Node::TYPE_FILE:
-                if (! $handle = $this->_backend->fopen($_statpath, 'w')) {
-                    throw new Tinebase_Exception_AccessDenied('Permission denied to create file (filename ' . $_statpath . ')');
-                }
-                if ($_tempFileId !== NULL) {
-                    $this->_copyTempfile($_tempFileId, $handle);
-                    $this->_backend->clearStatCache($_statpath);
-                }
-                $this->_backend->fclose($handle);
+                $this->_backend->copyTempfile($_tempFileId, $_statpath);
                 break;
             case Tinebase_Model_Tree_Node::TYPE_FOLDER:
                 $this->_backend->mkdir($_statpath);
@@ -635,28 +628,6 @@ class Filemanager_Controller_Node extends Tinebase_Controller_Record_Abstract
     }
     
     /**
-     * copy tempfile data to file handle
-     * 
-     * @param string $_tempFileId
-     * @param resource $_fileHandle
-     * @throws Filemanager_Exception
-     */
-    protected function _copyTempfile($_tempFileId, $_fileHandle)
-    {
-        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
-            ' Reading data from tempfile ...');
-        
-        $tempFile = Tinebase_TempFile::getInstance()->getTempFile($_tempFileId);
-        $tempData = fopen($tempFile->path, 'r');
-        if ($tempData) {
-            stream_copy_to_stream($tempData, $_fileHandle);
-            fclose($tempData);
-        } else {
-            throw new Filemanager_Exception('Could not read tempfile ' . $tempFile->path);
-        }
-    }
-    
-    /**
      * check file existance
      * 
      * @param Tinebase_Model_Tree_Node_Path $_path
index 6de8aee..07d167d 100644 (file)
@@ -29,12 +29,7 @@ class Filemanager_Frontend_Http extends Tinebase_Frontend_Http_Abstract
      */
     public function downloadFile($path, $id)
     {
-        $oldMaxExcecutionTime = Tinebase_Core::setExecutionLifeTime(0);
         $nodeController = Filemanager_Controller_Node::getInstance();
-        
-        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' '
-            . ' Download file ' . $path ? $path : $id
-        );
         if ($path) {
             $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath($nodeController->addBasePath($path));
             $node = $nodeController->getFileNode($pathRecord);
@@ -46,23 +41,7 @@ class Filemanager_Frontend_Http extends Tinebase_Frontend_Http_Abstract
             Tinebase_Exception_InvalidArgument('Either a path or id is needed to download a file.');
         }
         
-        // cache for 3600 seconds
-        $maxAge = 3600;
-        header('Cache-Control: private, max-age=' . $maxAge);
-        header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT");
-        
-        // overwrite Pragma header from session
-        header("Pragma: cache");
-        
-        header('Content-Disposition: attachment; filename="' . $node->name . '"');
-        header("Content-Type: " . $node->contenttype);
-        
-        $handle = fopen($pathRecord->streamwrapperpath, 'r');
-        fpassthru($handle);
-        fclose($handle);
-
-        Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
-        
+        $this->_downloadFileNode($node, $pathRecord->streamwrapperpath);
         exit;
     }
 }
index 710b2f9..cce54c3 100644 (file)
@@ -94,40 +94,6 @@ class Tasks_Frontend_Json extends Tinebase_Frontend_Json_Abstract
     }
     
     /**
-     * returns record prepared for json transport
-     *
-     * @param Tinebase_Record_Interface $_record
-     * @return array record data
-     */
-    protected function _recordToJson($_record)
-    {
-        if ($_record instanceof Tasks_Model_Task) {
-            Tinebase_User::getInstance()->resolveUsers($_record, 'organizer', true);
-        }
-        
-        return parent::_recordToJson($_record);
-    }    
-    
-    /**
-     * returns multiple records prepared for json transport
-     *
-     * @param Tinebase_Record_RecordSet $_records Tinebase_Record_Abstract
-     * @param Tinebase_Model_Filter_FilterGroup
-     * @param Tinebase_Model_Pagination $_pagination
-     * @return array data
-     */
-    protected function _multipleRecordsToJson(Tinebase_Record_RecordSet $_records, $_filter = NULL, $_pagination = NULL)
-    {
-        if ($_records->getRecordClassName() == 'Tasks_Model_Task') {
-            // NOTE: in contrast to calendar, organizers in tasks are accounts atm.
-            Tinebase_User::getInstance()->resolveMultipleUsers($_records, 'organizer', true);
-        }
-        
-        //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(print_r($_records->toArray(), true));
-        return parent::_multipleRecordsToJson($_records, $_filter, $_pagination);
-    }    
-    
-    /**
      * Deletes an existing Task
      *
      * @param array $ids 
index ec35bb9..751368c 100644 (file)
@@ -30,6 +30,21 @@ class Tasks_Model_Task extends Tinebase_Record_Abstract
     protected $_identifier = 'id';
     
     /**
+     * if foreign Id fields should be resolved on search and get from json
+     * should have this format: 
+     *     array('Calendar_Model_Contact' => 'contact_id', ...)
+     * or for more fields:
+     *     array('Calendar_Model_Contact' => array('contact_id', 'customer_id), ...)
+     * (e.g. resolves contact_id with the corresponding Model)
+     * 
+     * @var array
+     */
+    protected static $_resolveForeignIdFields = array(
+        'Tinebase_Model_User'     => array('created_by', 'last_modified_by', 'organizer'),
+        'recursive'               => array('attachments' => 'Tinebase_Model_Tree_Node'),
+    );
+    
+    /**
      * application the record belongs to
      *
      * @var string
@@ -43,56 +58,57 @@ class Tasks_Model_Task extends Tinebase_Record_Abstract
      */
     protected $_validators = array(
         // tine record fields
-        'container_id'         => array('allowEmpty' => true,  'Int' ),
-        'created_by'           => array('allowEmpty' => true,        ),
-        'creation_time'        => array('allowEmpty' => true         ),
-        'last_modified_by'     => array('allowEmpty' => true         ),
-        'last_modified_time'   => array('allowEmpty' => true         ),
-        'is_deleted'           => array('allowEmpty' => true         ),
-        'deleted_time'         => array('allowEmpty' => true         ),
-        'deleted_by'           => array('allowEmpty' => true         ),
-        'seq'                  => array('allowEmpty' => true         ),
+        'container_id'         => array(Zend_Filter_Input::ALLOW_EMPTY => true,  'Int' ),
+        'created_by'           => array(Zend_Filter_Input::ALLOW_EMPTY => true,        ),
+        'creation_time'        => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'last_modified_by'     => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'last_modified_time'   => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'is_deleted'           => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'deleted_time'         => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'deleted_by'           => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'seq'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         // task only fields
-        'id'                   => array('allowEmpty' => true, 'Alnum'),
-        'percent'              => array('allowEmpty' => true, 'default' => 0),
-        'completed'            => array('allowEmpty' => true         ),
-        'due'                  => array('allowEmpty' => true         ),
+        'id'                   => array(Zend_Filter_Input::ALLOW_EMPTY => true, 'Alnum'),
+        'percent'              => array(Zend_Filter_Input::ALLOW_EMPTY => true, 'default' => 0),
+        'completed'            => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'due'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         // ical common fields
         'class'                => array(
-            'allowEmpty' => true,
+            Zend_Filter_Input::ALLOW_EMPTY => true,
             array('InArray', array(self::CLASS_PUBLIC, self::CLASS_PRIVATE, /*self::CLASS_CONFIDENTIAL*/)),
         ),
-        'description'          => array('allowEmpty' => true         ),
-        'geo'                  => array('allowEmpty' => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
-        'location'             => array('allowEmpty' => true         ),
-        'organizer'            => array('allowEmpty' => true,        ),
-        'originator_tz'        => array('allowEmpty' => true         ),
-        'priority'             => array('allowEmpty' => true, 'default' => 1),
-        'status'               => array('allowEmpty' => true         ),
+        'description'          => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'geo'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
+        'location'             => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'organizer'            => array(Zend_Filter_Input::ALLOW_EMPTY => true,        ),
+        'originator_tz'        => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
+        'priority'             => array(Zend_Filter_Input::ALLOW_EMPTY => true, 'default' => 1),
+        'status'               => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         'summary'              => array('presence' => 'required'     ),
-        'url'                  => array('allowEmpty' => true         ),
+        'url'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true         ),
         // ical common fields with multiple appearance
-        'attach'                => array('allowEmpty' => true        ),
-        'attendee'              => array('allowEmpty' => true        ),
-        'tags'                  => array('allowEmpty' => true        ), //originally categories
-        'comment'               => array('allowEmpty' => true        ),
-        'contact'               => array('allowEmpty' => true        ),
-        'related'               => array('allowEmpty' => true        ),
-        'resources'             => array('allowEmpty' => true        ),
-        'rstatus'               => array('allowEmpty' => true        ),
+        'attach'               => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'attendee'             => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'tags'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true        ), //originally categories
+        'comment'              => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'contact'              => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'related'              => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'resources'            => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'rstatus'              => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
         // scheduleable interface fields
-        'dtstart'               => array('allowEmpty' => true        ),
-        'duration'              => array('allowEmpty' => true        ),
-        'recurid'               => array('allowEmpty' => true        ),
+        'dtstart'              => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'duration'             => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'recurid'              => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
         // scheduleable interface fields with multiple appearance
-        'exdate'                => array('allowEmpty' => true        ),
-        'exrule'                => array('allowEmpty' => true        ),
-        'rdate'                 => array('allowEmpty' => true        ),
-        'rrule'                 => array('allowEmpty' => true        ),
+        'exdate'               => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'exrule'               => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'rdate'                => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'rrule'                => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
         // tine 2.0 notes, alarms and relations
-        'notes'                 => array('allowEmpty' => true        ),
-        'alarms'                => array('allowEmpty' => true        ), // RecordSet of Tinebase_Model_Alarm
-        'relations'             => array('allowEmpty' => true        ),
+        'notes'                => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'alarms'               => array(Zend_Filter_Input::ALLOW_EMPTY => true        ), // RecordSet of Tinebase_Model_Alarm
+        'relations'            => array(Zend_Filter_Input::ALLOW_EMPTY => true        ),
+        'attachments'          => array(Zend_Filter_Input::ALLOW_EMPTY => true),
     );
     
     /**
index fe7342b..ef90357 100644 (file)
@@ -50,7 +50,8 @@ Tine.Tasks.Model.TaskArray = Tine.Tinebase.Model.genericFields.concat([
     // tine 2.0 alarms field
     { name: 'alarms'},
     // relations with other objects
-    { name: 'relations'}
+    { name: 'relations'},
+    { name: 'attachments'}
 ]);
 
 /**
index 11e44fd..4549851 100644 (file)
@@ -6,7 +6,7 @@
  * @subpackage  Controller
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Philipp Schüle <p.schuele@metaways.de>
- * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
  *
  * @todo        this should be splitted into smaller parts!
  */
@@ -366,6 +366,9 @@ abstract class Tinebase_Controller_Record_Abstract
         if ($this->resolveCustomfields()) {
             Tinebase_CustomField::getInstance()->resolveRecordCustomFields($record);
         }
+        if ($record->has('attachments')) {
+            Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachments($record);
+        }
     }
 
     /**
@@ -780,7 +783,11 @@ abstract class Tinebase_Controller_Record_Abstract
         if ($record->has('alarms') && isset($record->alarms)) {
             $this->_saveAlarms($record);
         }
-
+        if ($record->has('attachments') && isset($record->attachments)) {
+            $updatedRecord->attachments = $record->attachments;
+            Tinebase_FileSystem_RecordAttachments::getInstance()->setRecordAttachments($updatedRecord);
+        }
+        
         if ($returnUpdatedRelatedData) {
             $this->_getRelatedData($updatedRecord);
         }
@@ -1269,6 +1276,9 @@ abstract class Tinebase_Controller_Record_Abstract
                 }
             }
         }
+        if ($_record->has('attachments')) {
+            Tinebase_FileSystem_RecordAttachments::getInstance()->deleteRecordAttachments($_record);
+        }
     }
 
     /**
index 73c0275..32fa568 100644 (file)
@@ -45,7 +45,7 @@ class Tinebase_Convert_Json implements Tinebase_Convert_Interface
         $records = new Tinebase_Record_RecordSet(get_class($_record), array($_record));
         
         Tinebase_Frontend_Json_Abstract::resolveContainerTagsUsers($records);
-        $this->_resolveMultipleIdFields($records);
+        self::resolveMultipleIdFields($records);
         
         $_record = $records->getFirstRecord();
         
@@ -57,35 +57,93 @@ class Tinebase_Convert_Json implements Tinebase_Convert_Interface
 
     /**
      * resolves multiple records
-     * @param Tinebase_Record_RecordSet $_records the records
+     * 
+     * @param Tinebase_Record_RecordSet $records the records
+     * @param array $resolveFields
      */
-    protected function _resolveMultipleIdFields(Tinebase_Record_RecordSet $_records)
+    public static function resolveMultipleIdFields($records, $resolveFields = NULL)
     {
-        $ownRecordClass = $_records->getRecordClassName();
-        if(! $resolveFields = $ownRecordClass::getResolveForeignIdFields()) {
+        if (! $records instanceof Tinebase_Record_RecordSet) {
             return;
         }
         
-        foreach($resolveFields as $foreignRecordClassName => $fields) {
-            $foreignIds = array();
-            $fields = (array) $fields;
-            
-            foreach($fields as $field) {
-                $foreignIds = array_unique(array_merge($foreignIds, $_records->{$field}));
+        $ownRecordClass = $records->getRecordClassName();
+        if ($resolveFields === NULL) {
+            $resolveFields = $ownRecordClass::getResolveForeignIdFields();
+        }
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
+            . ' Resolving ' . $ownRecordClass . ' fields: ' . print_r($resolveFields, TRUE));
+        
+        foreach ((array) $resolveFields as $foreignRecordClassName => $fields) {
+            if ($foreignRecordClassName === 'recursive') {
+                foreach ($fields as $field => $model) {
+                    foreach ($records->$field as $subRecords) {
+                        self::resolveMultipleIdFields($subRecords);
+                    }
+                }
+            } else {
+                self::_resolveForeignIdFields($records, $foreignRecordClassName, (array) $fields);
             }
-            
-            if (! Tinebase_Core::getUser()->hasRight(substr($foreignRecordClassName, 0, strpos($foreignRecordClassName, "_")), Tinebase_Acl_Rights_Abstract::RUN))
-                continue;
-            
-            $controller = Tinebase_Core::getApplicationInstance($foreignRecordClassName);
-            
+        }
+    }
+    
+    /**
+     * resolve foreign fields for records
+     * 
+     * @param Tinebase_Record_RecordSet $records
+     * @param string $foreignRecordClassName
+     * @param array $fields
+     */
+    protected static function _resolveForeignIdFields($records, $foreignRecordClassName, $fields)
+    {
+        $options = array_key_exists('options', $fields) ? $fields['options'] : array();
+        $fields = array_key_exists('fields', $fields) ? $fields['fields'] : $fields;
+        
+        $foreignIds = array();
+        foreach ($fields as $field) {
+            $foreignIds = array_unique(array_merge($foreignIds, $records->{$field}));
+        }
+        
+        if (! Tinebase_Core::getUser()->hasRight(substr($foreignRecordClassName, 0, strpos($foreignRecordClassName, "_")), Tinebase_Acl_Rights_Abstract::RUN)) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ 
+                . ' Not resolving ' . $foreignRecordClassName . ' records because user has no right to run app.');
+            return;
+        }
+        
+        $controller = Tinebase_Core::getApplicationInstance($foreignRecordClassName);
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
+            . ' Fetching ' . $foreignRecordClassName . ' by id: ' . print_r($foreignIds, TRUE));
+        
+        if (array_key_exists('ignoreAcl', $options) && $options['ignoreAcl']) {
+            // @todo make sure that second param of getMultiple() is $ignoreAcl
+            $foreignRecords = $controller->getMultiple($foreignIds, TRUE);
+        } else {
             $foreignRecords = $controller->getMultiple($foreignIds);
-            if($foreignRecords->count()) {
-                foreach ($_records as $record) {
-                    foreach($fields as $field) {
-                        $idx = $foreignRecords->getIndexById($record->{$field});
-                        if(isset($idx) && $idx !== FALSE) {
-                            $record->{$field} = $foreignRecords[$idx];
+        }
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ 
+            . ' Foreign records found: ' . print_r($foreignRecords->toArray(), TRUE));
+        
+        if (count($foreignRecords) === 0) {
+            return;
+        }
+        
+        foreach ($records as $record) {
+            foreach ($fields as $field) {
+                if (is_scalar($record->{$field})) {
+                    $idx = $foreignRecords->getIndexById($record->{$field});
+                    if (isset($idx) && $idx !== FALSE) {
+                        $record->{$field} = $foreignRecords[$idx];
+                    } else {
+                        switch ($foreignRecordClassName) {
+                            case 'Tinebase_Model_User':
+                            case 'Tinebase_Model_FullUser':
+                                $record->{$field} = Tinebase_User::getInstance()->getNonExistentUser();
+                                break;
+                            default:
+                                // skip
                         }
                     }
                 }
@@ -110,7 +168,7 @@ class Tinebase_Convert_Json implements Tinebase_Convert_Interface
 
         Tinebase_Frontend_Json_Abstract::resolveContainerTagsUsers($_records);
 
-        $this->_resolveMultipleIdFields($_records);
+        self::resolveMultipleIdFields($_records);
 
         $_records->setTimezone(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
         $_records->convertDates = true;
index b6aadff..484771f 100644 (file)
@@ -6,7 +6,7 @@
  * @subpackage  FileSystem
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Lars Kneschke <l.kneschke@metaways.de>
- * @copyright   Copyright (c) 2010-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2010-2013 Metaways Infosystems GmbH (http://www.metaways.de)
  * 
  * @todo 0007376: Tinebase_FileSystem / Node model refactoring: move all container related functionality to Filemanager
  */
 class Tinebase_FileSystem implements Tinebase_Controller_Interface
 {
     /**
+     * folder name/type for record attachments
+     * 
+     * @var string
+     */
+    const FOLDER_TYPE_RECORDS = 'records';
+    
+    /**
      * @var Tinebase_Tree_FileObject
      */
     protected $_fileObjectBackend;
@@ -112,11 +119,12 @@ class Tinebase_FileSystem implements Tinebase_Controller_Interface
      */
     public function getApplicationBasePath($_application, $_type = NULL)
     {
-        $application = $_application instanceof Tinebase_Model_Application ? $_application : Tinebase_Application::getInstance()->getApplicationById($_application);
+        $application = $_application instanceof Tinebase_Model_Application 
+            ? $_application : Tinebase_Application::getInstance()->getApplicationById($_application);
         
         $result = '/' . $application->getId();
         if ($_type !== NULL) {
-            if (! in_array($_type, array(Tinebase_Model_Container::TYPE_SHARED, Tinebase_Model_Container::TYPE_PERSONAL))) {
+            if (! in_array($_type, array(Tinebase_Model_Container::TYPE_SHARED, Tinebase_Model_Container::TYPE_PERSONAL, self::FOLDER_TYPE_RECORDS))) {
                 throw new Tinebase_Exception_UnexpectedValue('Type can only be shared or personal.');
             }
             
@@ -512,7 +520,8 @@ class Tinebase_FileSystem implements Tinebase_Controller_Interface
      */
     public function mkdir($_path)
     {
-        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Creating directory ' . $_path);
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . ' Creating directory ' . $_path);
         
         $path = '/';
         $parentNode = null;
@@ -542,7 +551,7 @@ class Tinebase_FileSystem implements Tinebase_Controller_Interface
         
         $node = $this->stat($_path);
         
-        $children = $this->_getTreeNodeChildren($node);
+        $children = $this->getTreeNodeChildren($node);
         
         // check if child entries exists and delete if $_recursive is true
         if (count($children) > 0) {
@@ -580,7 +589,7 @@ class Tinebase_FileSystem implements Tinebase_Controller_Interface
     {
         $node = $this->stat($_path);
         
-        $children = $this->_getTreeNodeChildren($node);
+        $children = $this->getTreeNodeChildren($node);
         
         foreach ($children as $child) {
             $this->_statCache[$_path . '/' . $child->name] = $child;
@@ -717,22 +726,28 @@ class Tinebase_FileSystem implements Tinebase_Controller_Interface
     /**
      * get tree node children
      * 
-     * @param string $_nodeId
+     * @param string|Tinebase_Model_Tree_Node|Tinebase_Record_RecordSet $_nodeId
      * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
      */
-    protected function _getTreeNodeChildren($_nodeId)
+    public function getTreeNodeChildren($_nodeId)
     {
-        $nodeId = $_nodeId instanceof Tinebase_Model_Tree_Node ? $_nodeId->getId() : $_nodeId;
-        $children = array();
-        
+        if ($_nodeId instanceof Tinebase_Model_Tree_Node) {
+            $nodeId = $_nodeId->getId();
+            $operator = 'equals';
+        } else if ($_nodeId instanceof Tinebase_Record_RecordSet) {
+            $nodeId = $_nodeId->getArrayOfIds();
+            $operator = 'in';
+        } else {
+            $nodeId = $_nodeId;
+            $operator = 'equals';
+        }
         $searchFilter = new Tinebase_Model_Tree_Node_Filter(array(
             array(
                 'field'     => 'parent_id',
-                'operator'  => 'equals',
+                'operator'  => $operator,
                 'value'     => $nodeId
             )
         ));
-        
         $children = $this->searchNodes($searchFilter);
         
         return $children;
@@ -951,4 +966,34 @@ class Tinebase_FileSystem implements Tinebase_Controller_Interface
         
         return $deleteCount;
     }
+
+    /**
+     * copy tempfile data to file path
+     * 
+     * @param string|Tinebase_Model_TempFile $tempFile
+     * @param string $path
+     * @throws Tinebase_Exception_AccessDenied
+     */
+    public function copyTempfile($tempFile, $path)
+    {
+        if (! $handle = $this->fopen($path, 'w')) {
+            throw new Tinebase_Exception_AccessDenied('Permission denied to create file (filename ' . $path . ')');
+        }
+        
+        if ($tempFile !== NULL) {
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+                ' Reading data from tempfile ...');
+        
+            $tempFile = ($tempFile instanceof Tinebase_Model_TempFile) ? $tempFile : Tinebase_TempFile::getInstance()->getTempFile($tempFile);
+            $tempData = fopen($tempFile->path, 'r');
+            if ($tempData) {
+                stream_copy_to_stream($tempData, $handle);
+                fclose($tempData);
+            } else {
+                throw new Tinebase_Exception_AccessDenied('Could not read tempfile ' . $tempFile->path);
+            }
+            $this->clearStatCache($path);
+        }
+        $this->fclose($handle);
+    }
 }
diff --git a/tine20/Tinebase/FileSystem/RecordAttachments.php b/tine20/Tinebase/FileSystem/RecordAttachments.php
new file mode 100644 (file)
index 0000000..2278780
--- /dev/null
@@ -0,0 +1,224 @@
+<?php
+/**
+ * Tine 2.0
+ *
+ * @package     Tinebase
+ * @subpackage  Filesystem
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ * @copyright   Copyright (c) 2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ * 
+ */
+
+/**
+ * filesystem attachments for records
+ *
+ * @package     Tinebase
+ * @subpackage  Filesystem
+ */
+class Tinebase_FileSystem_RecordAttachments
+{
+    /**
+     * filesystem controller
+     * 
+     * @var Tinebase_FileSystem
+     */
+    protected $_fsController = NULL;
+    
+    /**
+     * holds the instance of the singleton
+     *
+     * @var Tinebase_FileSystem_RecordAttachments
+     */
+    private static $_instance = NULL;
+    
+    /**
+     * the constructor
+     */
+    public function __construct() 
+    {
+        $this->_fsController  = Tinebase_FileSystem::getInstance();
+    }
+    
+    /**
+     * the singleton pattern
+     *
+     * @return Tinebase_FileSystem_RecordAttachments
+     */
+    public static function getInstance() 
+    {
+        if (self::$_instance === NULL) {
+            self::$_instance = new Tinebase_FileSystem_RecordAttachments();
+        }
+        
+        return self::$_instance;
+    }
+    
+    /**
+     * fetch all file attachments of a record
+     * 
+     * @param Tinebase_Record_Abstract $record
+     * @return Tinebase_Record_RecordSet of Tinebase_Model_Tree_Node
+     */
+    public function getRecordAttachments(Tinebase_Record_Abstract $record)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+            ' Fetching attachments of ' . get_class($record) . ' record with id ' . $record->getId() . ' ...');
+        
+        $parentPath = $this->getRecordAttachmentPath($record);
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
+            ' Looking in path ' . $parentPath);
+        
+        try {
+            $parentNode = $this->_fsController->stat($parentPath);
+            $record->attachments = $this->_fsController->getTreeNodeChildren($parentNode);
+        } catch (Tinebase_Exception_NotFound $tenf) {
+            $record->attachments = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
+        }
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+            ' Found ' . count($record->attachments) . ' attachment(s).');
+        
+        return $record->attachments;
+    }
+    
+    /**
+     * fetches attachments for multiple records at once
+     * 
+     * @param Tinebase_Record_RecordSet $records
+     * 
+     * @todo maybe this should be improved
+     */
+    public function getMultipleAttachmentsOfRecords($records)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+            ' Fetching attachments for ' . count($records) . ' record(s)');
+        
+        $parentNodes = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
+        $recordNodeMapping = array();
+        foreach ($records as $record) {
+            $parentPath = $this->getRecordAttachmentPath($record);
+            try {
+                $node = $this->_fsController->stat($parentPath);
+                $parentNodes->addRecord($node);
+                $recordNodeMapping[$node->getId()] = $record->getId();
+            } catch (Tinebase_Exception_NotFound $tenf) {
+                $record->attachments = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
+            }
+        }
+        
+        $children = $this->_fsController->getTreeNodeChildren($parentNodes);
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+            ' Found ' . count($children) . ' attachment(s).');
+        
+        foreach ($children as $node) {
+            $record = $records->getById($recordNodeMapping[$node->parent_id]);
+            if (! isset($record->attachments)) {
+                $record->attachments = new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
+            }
+            $record->attachments->addRecord($node);
+        }
+    }
+    
+    /**
+     * set file attachments of a record
+     * 
+     * @param Tinebase_Record_Abstract $record
+     */
+    public function setRecordAttachments(Tinebase_Record_Abstract $record)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
+            ' Record: ' . print_r($record->toArray(), TRUE));
+
+        $currentAttachments = ($record->getId()) ? $this->getRecordAttachments(clone $record) : new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node');
+        $attachmentsToSet = ($record->attachments instanceof Tinebase_Record_RecordSet) 
+            ? $record->attachments
+            : new Tinebase_Record_RecordSet('Tinebase_Model_Tree_Node', (array)$record->attachments, TRUE);
+        
+        $attachmentDiff = $currentAttachments->diff($attachmentsToSet);
+        
+        foreach ($attachmentDiff->added as $added) {
+            if (isset($added->tempFile)) {
+                $tempFile = ($added->tempFile instanceof Tinebase_Model_TempFile) 
+                    ? $added->tempFile : new Tinebase_Model_TempFile($added->tempFile, TRUE);
+                try {
+                    $this->_addAttachmentFromTempfile($record, $tempFile);
+                } catch (Tinebase_Exception_NotFound $tenf) {
+                    if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+                        ' Record: ' . print_r($record->toArray(), TRUE));
+                    if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
+                        ' Could not add new attachment to record: ' . $tenf);
+                }
+            }
+            
+        }
+        
+        foreach ($attachmentDiff->removed as $removed) {
+            $this->_fsController->deleteFileNode($removed);
+        }
+
+        foreach ($attachmentDiff->modified as $modified) {
+            $this->_fsController->update($attachmentsToSet->getById($modified->getId()));
+        }
+    }
+    
+    /**
+     * add attachment from tempfile
+     * 
+     * @param Tinebase_Record_Abstract $record
+     * @param Tinebase_Model_TempFile $tempFile
+     * @throws Tinebase_Exception_InvalidArgument
+     */
+    protected function _addAttachmentFromTempfile(Tinebase_Record_Abstract $record, $tempFile)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
+            ' Creating new record attachment from tempfile');
+        
+        $tempFile = Tinebase_TempFile::getInstance()->get($tempFile->getId());
+        $attachmentsDir = $this->getRecordAttachmentPath($record, TRUE);
+        $attachmentPath = $attachmentsDir . '/' . $tempFile->name;
+        if ($this->_fsController->fileExists($attachmentPath)) {
+            throw new Tinebase_Exception_InvalidArgument('file already exists');
+        }
+        
+        $this->_fsController->copyTempfile($tempFile, $attachmentPath);
+    }
+    
+    /**
+     * delete attachments of record
+     * 
+     * @param Tinebase_Record_Abstract $record
+     */
+    public function deleteRecordAttachments($record)
+    {
+        $attachments = ($record->attachments instanceof Tinebase_Record_RecordSet) ? $record->attachments : $this->getRecordAttachments($record);
+        foreach ($attachments as $node) {
+            $this->_fsController->deleteFileNode($node);
+        }
+    }
+    
+    /**
+     * get path for record attachments
+     * 
+     * @param Tinebase_Record_Abstract $record
+     * @param boolean $createDirIfNotExists
+     * @throws Tinebase_Exception_InvalidArgument
+     * @return string
+     */
+    public function getRecordAttachmentPath(Tinebase_Record_Abstract $record, $createDirIfNotExists = FALSE)
+    {
+        if (! $record->getId()) {
+            throw new Tinebase_Exception_InvalidArgument('record needs an identifier');
+        }
+        
+        $parentPath = $this->_fsController->getApplicationBasePath($record->getApplication(), Tinebase_FileSystem::FOLDER_TYPE_RECORDS);
+        $recordPath = $parentPath . '/' . get_class($record) . '/' . $record->getId();
+        if ($createDirIfNotExists && ! $this->_fsController->fileExists($recordPath)) {
+            $this->_fsController->mkdir($recordPath);
+        }
+        
+        return $recordPath;
+    }
+}
index a465526..eef9fd7 100644 (file)
@@ -803,4 +803,27 @@ class Tinebase_Frontend_Http extends Tinebase_Frontend_Http_Abstract
         
     }
     
+    /**
+     * download file attachment
+     * 
+     * @param string $nodeId
+     * @param string $recordId
+     * @param string $modelName
+     */
+    public function downloadRecordAttachment($nodeId, $recordId, $modelName)
+    {
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . ' Downloading attachment of ' . $modelName . ' record with id ' . $recordId);
+        
+        $recordController = Tinebase_Core::getApplicationInstance($modelName);
+        $record = $recordController->get($recordId);
+        
+        $node = Tinebase_FileSystem::getInstance()->get($nodeId);
+        $path = Tinebase_Model_Tree_Node_Path::STREAMWRAPPERPREFIX
+            . Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachmentPath($record)
+            . '/' . $node->name;
+        
+        $this->_downloadFileNode($node, $path);
+        exit;
+    }
 }
index 640fed7..63345d5 100644 (file)
@@ -106,4 +106,35 @@ abstract class Tinebase_Frontend_Http_Abstract extends Tinebase_Frontend_Abstrac
         // reset max execution time to old value
         Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
     }
+
+    /**
+     * download (fpassthru) file node
+     * 
+     * @param Tinebase_Model_Tree_Node $node
+     * @param string $filesystemPath
+     */
+    protected function _downloadFileNode($node, $filesystemPath)
+    {
+        $oldMaxExcecutionTime = Tinebase_Core::setExecutionLifeTime(0);
+        
+        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
+            . ' Download file node ' . print_r($node->toArray(), TRUE));
+        
+        // cache for 3600 seconds
+        $maxAge = 3600;
+        header('Cache-Control: private, max-age=' . $maxAge);
+        header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT");
+        
+        // overwrite Pragma header from session
+        header("Pragma: cache");
+        
+        header('Content-Disposition: attachment; filename="' . $node->name . '"');
+        header("Content-Type: " . $node->contenttype);
+        
+        $handle = fopen($filesystemPath, 'r');
+        fpassthru($handle);
+        fclose($handle);
+
+        Tinebase_Core::setExecutionLifeTime($oldMaxExcecutionTime);
+    }
 }
index 1669f1a..f0106cf 100644 (file)
@@ -160,6 +160,8 @@ abstract class Tinebase_Frontend_Json_Abstract extends Tinebase_Frontend_Abstrac
      *
      * @param Tinebase_Record_RecordSet $_records
      * @param array $_resolveProperties
+     * 
+     * @todo rename function as this no longer resolves users!
      */
     public static function resolveContainerTagsUsers(Tinebase_Record_RecordSet $_records, $_resolveProperties = array('container_id', 'tags'))
     {
@@ -169,7 +171,7 @@ abstract class Tinebase_Frontend_Json_Abstract extends Tinebase_Frontend_Abstrac
             if ($firstRecord->has('container_id') && in_array('container_id', $_resolveProperties)) {
                 Tinebase_Container::getInstance()->getGrantsOfRecords($_records, Tinebase_Core::getUser());
             }
-        
+            
             if ($firstRecord->has('tags') && in_array('tags', $_resolveProperties)) {
                 Tinebase_Tags::getInstance()->getMultipleTagsOfRecords($_records);
             }
index 1ee5918..484bfb8 100644 (file)
@@ -108,9 +108,10 @@ class Tinebase_Model_Tree_Node extends Tinebase_Record_Abstract
         'size'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
     // not persistent
         'container_name' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
-        // this is needed for ACL handling and should be sent by / delivered to client (not persistent in db atm)
+        // this is needed should be sent by / delivered to client (not persistent in db atm)
         'path'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
         'account_grants' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
+        'tempFile'       => array(Zend_Filter_Input::ALLOW_EMPTY => true),
     );
     
     /**
index 153ebf4..46c6ef1 100644 (file)
@@ -62,17 +62,21 @@ class Tinebase_Record_RecordSet implements IteratorAggregate, Countable, ArrayAc
      * creates new Tinebase_Record_RecordSet
      *
      * @param string $_className the required classType
-     * @param array $_records array of record objects
+     * @param array|Tinebase_Record_RecordSet $_records array of record objects
      * @param bool $_bypassFilters {@see Tinebase_Record_Interface::__construct}
      * @param bool $_convertDates {@see Tinebase_Record_Interface::__construct}
      * @return void
+     * @throws Tinebase_Exception_InvalidArgument
      */
-    public function __construct($_className, array $_records = array(),  $_bypassFilters = false, $_convertDates = true)
+    public function __construct($_className, $_records = array(), $_bypassFilters = false, $_convertDates = true)
     {
+        if (! class_exists($_className)) {
+            throw new Tinebase_Exception_InvalidArgument('Class ' . $_className . ' does not exist');
+        }
         $this->_recordClass = $_className;
-
-        foreach($_records as $record) {
-            $toAdd = is_array($record) ? new $this->_recordClass($record, $_bypassFilters, $_convertDates) : $record;
+        
+        foreach ($_records as $record) {
+            $toAdd = $record instanceof Tinebase_Record_Abstract ? $record : new $this->_recordClass($record, $_bypassFilters, $_convertDates);
             $this->addRecord($toAdd);
         }
     }
index 4e4f567..f1fbd12 100644 (file)
           "path": "js/widgets/grid/"
         },
         {
+          "text": "AttachmentsGridPanel.js",
+          "path": "js/widgets/dialog/"
+        },
+        {
           "text": "PickerGridPanel.js",
           "path": "js/widgets/grid/"
         },
index ad436eb..6d3c473 100644 (file)
@@ -932,7 +932,7 @@ class Tinebase_User_Sql extends Tinebase_User_Abstract
     /**
      * Get multiple users
      *
-     * @param     string|array $_id Ids
+     * @param   string|array $_id Ids
      * @param   string  $_accountClass  type of model to return
      * @return Tinebase_Record_RecordSet of 'Tinebase_Model_User' or 'Tinebase_Model_FullUser'
      */
@@ -941,8 +941,8 @@ class Tinebase_User_Sql extends Tinebase_User_Abstract
         if (empty($_id)) {
             return new Tinebase_Record_RecordSet($_accountClass);
         }
-
-        $select = $this->_getUserSelectObject()            
+        
+        $select = $this->_getUserSelectObject()
             ->where($this->_db->quoteIdentifier(SQL_TABLE_PREFIX . 'accounts.id') . ' in (?)', (array) $_id);
         
         $stmt = $this->_db->query($select);
index d51c114..e6103ba 100644 (file)
@@ -772,6 +772,16 @@ span:hover.tinebase-download-link {
     color: #4444ff;
 }
 
+.action_download {
+    background-image:url(../../images/oxygen/16x16/actions/save-all.png) !important;
+}
+.x-btn-medium .action_download {
+    background-image:url(../../images/oxygen/22x22/actions/save-all.png) !important;
+}
+.x-btn-large .action_download {
+    background-image:url(../../images/oxygen/32x32/actions/save-all.png) !important;
+}
+
 /********* uploads ************/
 
 .x-tinebase-uploading {
index 0ed065c..789b844 100644 (file)
@@ -448,3 +448,31 @@ Tine.Tinebase.Model.Config = Tine.Tinebase.data.Record.create([
     recordName: 'Config',
     recordsName: 'Configs'
 });
+
+
+/**
+ * @namespace   Tine.Tinebase.Model
+ * @class       Tine.Tinebase.Model.Node
+ * @extends     Tine.Tinebase.data.Record
+ */
+Tine.Tinebase.Model.Node = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model.modlogFields.concat([
+    { name: 'id' },
+    { name: 'name' },
+    { name: 'path' },
+    { name: 'size' },
+    { name: 'revision' },
+    { name: 'type' },
+    { name: 'contenttype' },
+    { name: 'description' },
+    { name: 'account_grants' },
+    { name: 'description' },
+    { name: 'object_id'}
+]), {
+    appName: 'Tinebase',
+    modelName: 'Node',
+    idProperty: 'id',
+    titleProperty: 'name',
+    // ngettext('File', 'Files', n); gettext('File');
+    recordName: 'File',
+    recordsName: 'Files'
+});
diff --git a/tine20/Tinebase/js/widgets/dialog/AttachmentsGridPanel.js b/tine20/Tinebase/js/widgets/dialog/AttachmentsGridPanel.js
new file mode 100644 (file)
index 0000000..77c670b
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ * Tine 2.0
+ *
+ * @package     Tinebase
+ * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ * @copyright   Copyright (c) 2013 Metaways Infosystems GmbH (http://www.metaways.de)
+ *
+ * TODO maybe we should generalize parts of this and have a common parent for this and Tine.widgets.relation.GenericPickerGridPanel
+ * TODO add more columns
+ * TODO allow to edit description
+ */
+Ext.ns('Tine.widgets.dialog');
+
+/**
+ * @namespace   Tine.widgets.dialog
+ * @class       Tine.widgets.dialog.AttachmentsGridPanel
+ * @extends     Tine.widgets.grid.FileUploadGrid
+ * @author      Philipp Schüle <p.schuele@metaways.de>
+ */
+Tine.widgets.dialog.AttachmentsGridPanel = Ext.extend(Tine.widgets.grid.FileUploadGrid, {
+    /**
+     * @cfg for FileUploadGrid
+     */
+    filesProperty: 'attachments',
+    
+    /**
+     * The calling EditDialog
+     * @type Tine.widgets.dialog.EditDialog
+     */
+    editDialog: null,
+    
+    /**
+     * title
+     * 
+     * @type String
+     */
+    title: null,
+    
+    /**
+     * the record
+     * @type Record
+     */
+    record: null,
+
+    /**
+     * @type Tinebase.Application
+     */
+    app: null,
+    
+    /* config */
+    frame: true,
+    border: true,
+    autoScroll: true,
+    layout: 'fit',
+
+    /**
+     * initializes the component
+     */
+    initComponent: function() {
+        this.record = this.editDialog.record;
+        this.app = this.editDialog.app;
+        this.title = this.i18nTitle = _('Attachments');
+        
+        Tine.widgets.dialog.MultipleEditDialogPlugin.prototype.registerSkipItem(this);
+        
+        this.editDialog.on('save', this.onSaveRecord, this);
+        this.editDialog.on('load', this.onLoadRecord, this);
+
+        Tine.widgets.dialog.AttachmentsGridPanel.superclass.initComponent.call(this);
+        
+        this.initActions();
+    },
+    
+    /**
+     * get columns
+     * @return Array
+     */
+    getColumns: function() {
+        var columns = [{
+            resizable: true,
+            id: 'name',
+            dataIndex: 'name',
+            width: 150,
+            header: _('Name'),
+            renderer: Ext.ux.PercentRendererWithName,
+            sortable: true
+        }, {
+            resizable: true,
+            id: 'size',
+            dataIndex: 'size',
+            width: 50,
+            header: _('Size'),
+            renderer: Ext.util.Format.fileSize,
+            sortable: true
+        }, {
+            resizable: true,
+            id: 'contenttype',
+            dataIndex: 'contenttype',
+            width: 80,
+            header: _('Content Type'),
+            sortable: true
+        },{ id: 'creation_time',      header: _('Creation Time'),         dataIndex: 'creation_time',         renderer: Tine.Tinebase.common.dateRenderer,     width: 80,
+            sortable: true },
+          { id: 'created_by',         header: _('Created By'),            dataIndex: 'created_by',            renderer: Tine.Tinebase.common.usernameRenderer, width: 80,
+            sortable: true }
+        ];
+        
+        return columns;
+    },
+    
+    /**
+     * init store
+     * @private
+     */
+    initStore: function () {
+        this.store = new Ext.data.SimpleStore({
+            fields: Tine.Tinebase.Model.Node
+        });
+    },
+    
+    /**
+     * initActions
+     */
+    initActions: function () {
+        this.action_download = new Ext.Action({
+            requiredGrant: 'readGrant',
+            allowMultiple: false,
+            actionType: 'download',
+            text: _('Download'),
+            handler: this.onDownload,
+            iconCls: 'action_download',
+            scope: this,
+            disabled:true
+        });
+        this.actionUpdater.addActions([this.action_download]);
+        this.getTopToolbar().addItem(this.action_download);
+        this.contextMenu.addItem(this.action_download);
+        
+        this.on('rowdblclick', this.onDownload.createDelegate(this), this);
+    },
+    
+    /**
+     * is called from onApplyChanges of the edit dialog per save event
+     * 
+     * @param {Tine.widgets.dialog.EditDialog} dialog
+     * @param {Tine.Tinebase.data.Record} record
+     * @param {Function} ticket
+     * @return {Boolean}
+     */
+    onSaveRecord: function(dialog, record, ticket) {
+        var interceptor = ticket();
+
+        if (record.data.hasOwnProperty('attachments')) {
+            delete record.data.attachments;
+        }
+        var attachments = this.getData();
+        record.set('attachments', attachments);
+        
+        interceptor();
+    },
+    
+    /**
+     * updates the title ot the tab
+     * @param {Integer} count
+     */
+    updateTitle: function(count) {
+        count = Ext.isNumber(count) ? count : this.store.getCount();
+        this.setTitle((count > 0) ?  this.i18nTitle + ' (' + count + ')' : this.i18nTitle);
+    },
+    
+    /**
+     * populate store
+     * 
+     * @param {EditDialog} dialog
+     * @param {Record} record
+     * @param {Function} ticketFn
+     */
+    onLoadRecord: function(dialog, record, ticketFn) {
+        this.store.removeAll();
+        var interceptor = ticketFn();
+        var attachments = record.get('attachments');
+        if (attachments && attachments.length > 0) {
+            this.updateTitle(attachments.length);
+            var attachmentRecords = [];
+            
+            Ext.each(attachments, function(attachment) {
+                attachmentRecords.push(new Tine.Tinebase.Model.Node(attachment, attachment.id));
+            }, this);
+            this.store.add(attachmentRecords);
+        }
+        
+        // add other listeners after population
+        if (this.store) {
+            this.store.on('update', this.updateTitle, this);
+            this.store.on('add', this.updateTitle, this);
+            this.store.on('remove', this.updateTitle, this);
+        }
+        interceptor();
+    },
+
+    /**
+     * get attachments data as array
+     * 
+     * @return {Array}
+     */
+    getData: function() {
+        var attachments = [];
+        
+        this.store.each(function(attachment) {
+            attachments.push(attachment.data);
+        }, this);
+
+        return attachments;
+    },
+    
+    /**
+     * download file
+     * 
+     * @param {} button
+     * @param {} event
+     */
+    onDownload: function(button, event) {
+        var selectedRows = this.getSelectionModel().getSelections(),
+            fileRow = selectedRows[0],
+            recordId = this.record.id;
+        
+        // TODO should be done by action updater
+        if (! recordId || Ext.isObject(fileRow.get('tempFile'))) {
+            Tine.log.debug('Tine.widgets.dialog.AttachmentsGridPanel::onDownload - file not yet available for download');
+            return;
+        }
+        
+        Tine.log.debug('Tine.widgets.dialog.AttachmentsGridPanel::onDownload - selected file:');
+        Tine.log.debug(fileRow);
+        
+        var downloader = new Ext.ux.file.Download({
+            params: {
+                method: 'Tinebase.downloadRecordAttachment',
+                requestType: 'HTTP',
+                nodeId: fileRow.id,
+                recordId: recordId,
+                modelName: this.app.name + '_Model_' + this.editDialog.modelName
+            }
+        }).start();
+    }
+});
index 64e616e..feff674 100644 (file)
@@ -3,7 +3,7 @@
  * 
  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
  * @author      Cornelius Weiss <c.weiss@metaways.de>
- * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
  */
 Ext.ns('Tine.widgets.dialog');
 
@@ -106,6 +106,12 @@ Tine.widgets.dialog.EditDialog = Ext.extend(Ext.FormPanel, {
     hideRelationsPanel: false,
     
     /**
+     * when a record has the attachments-property the attachments-panel can be disabled here
+     * @cfg {Boolean} hideAttachmentsPanel
+     */
+    hideAttachmentsPanel: false,
+    
+    /**
      * Registry for other relationgridpanels than the generic one,
      * handling special types of relations the generic one will not.
      * Panels registered here must have a store with the relation records.
@@ -158,10 +164,24 @@ Tine.widgets.dialog.EditDialog = Ext.extend(Ext.FormPanel, {
     deferredRender: false,
     buttonAlign: null,
     bufferResize: 500,
-    // the relationsPanel
+    
+    /**
+     * relations panel
+     * 
+     * @type Tine.widgets.relation.GenericPickerGridPanel
+     */
     relationsPanel: null,
+    
     // Array of Relation Pickers
     relationPickers: null,
+    
+    /**
+     * attachments panel
+     * 
+     * @type Tine.widgets.dialog.AttachmentsGridPanel
+     */
+    attachmentsPanel: null,
+    
     //private
     initComponent: function() {
         this.relationPanelRegistry = this.relationPanelRegistry ? this.relationPanelRegistry : [];
@@ -257,6 +277,8 @@ Tine.widgets.dialog.EditDialog = Ext.extend(Ext.FormPanel, {
         this.items = this.getFormItems();
         // init relations panel if relations are defined
         this.initRelationsPanel();
+        // init attachments panel
+        this.initAttachmentsPanel();
 
         Tine.widgets.dialog.EditDialog.superclass.initComponent.call(this);
         // set fields readOnly if set
@@ -458,12 +480,12 @@ Tine.widgets.dialog.EditDialog = Ext.extend(Ext.FormPanel, {
      */
     doCopyRecord: function() {
         var omitFields = this.recordClass.getMeta('copyOmitFields') || [];
-        // always omit id + notes
-        omitFields = omitFields.concat(['id', 'notes']);
+        // always omit id + notes + attachments
+        omitFields = omitFields.concat(['id', 'notes', 'attachments']);
         
         var fieldsToCopy = this.recordClass.getFieldNames().diff(omitFields),
             recordData = Ext.copyTo({}, this.record.data, fieldsToCopy);
-
+        
         this.record = new this.recordClass(recordData, 0);
     },
     
@@ -786,9 +808,19 @@ Tine.widgets.dialog.EditDialog = Ext.extend(Ext.FormPanel, {
      * creates the relations panel, if relations are defined
      */
     initRelationsPanel: function() {
-        if(!this.relationsPanel && !this.hideRelationsPanel && this.recordClass && this.recordClass.hasField('relations')) {
+        if (! this.relationsPanel && ! this.hideRelationsPanel && this.recordClass && this.recordClass.hasField('relations')) {
             this.relationsPanel = new Tine.widgets.relation.GenericPickerGridPanel({ anchor: '100% 100%', editDialog: this }); 
             this.items.items.push(this.relationsPanel);
         }
+    },
+    
+    /**
+     * creates attachments panel
+     */
+    initAttachmentsPanel: function() {
+        if (! this.attachmentsPanel && ! this.hideAttachmentsPanel && this.recordClass && this.recordClass.hasField('attachments')) {
+            this.attachmentsPanel = new Tine.widgets.dialog.AttachmentsGridPanel({ anchor: '100% 100%', editDialog: this }); 
+            this.items.items.push(this.attachmentsPanel);
+        }
     }
 });
index 78ceb43..69ad9ce 100644 (file)
@@ -93,8 +93,6 @@ Tine.widgets.grid.FileUploadGrid = Ext.extend(Ext.grid.GridPanel, {
             }
             this.contextMenu.showAt(e.getXY());
         }, this);
-        
-               
     },
     
     /**
@@ -188,7 +186,6 @@ Tine.widgets.grid.FileUploadGrid = Ext.extend(Ext.grid.GridPanel, {
             text: _('Pause upload'),
             iconCls: 'action_pause',
             scope: this,
-//            disabled: true,
             handler: this.onPause,
             actionUpdater: this.isPauseEnabled
         });
@@ -197,7 +194,6 @@ Tine.widgets.grid.FileUploadGrid = Ext.extend(Ext.grid.GridPanel, {
             text: _('Resume upload'),
             iconCls: 'action_resume',
             scope: this,
-//            disabled: true,
             handler: this.onResume,
             actionUpdater: this.isResumeEnabled
         });
@@ -217,7 +213,7 @@ Tine.widgets.grid.FileUploadGrid = Ext.extend(Ext.grid.GridPanel, {
         
         this.actionUpdater.addActions([
             this.action_pause,
-            this.action_resume                                      
+            this.action_resume
         ]);
 
     },