6589560f4bb249184ae888844fc637b1797ecf0f
[tine20] / tine20 / Tinebase / ModelConfiguration.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Tinebase
6  * @subpackage  Configuration
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2013-2015 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Alexander Stintzing <a.stintzing@metaways.de>
10  */
11
12 /**
13  * Tinebase_ModelConfiguration
14  *
15  * @package     Tinebase
16  * @subpackage  Configuration
17  *
18  */
19 class Tinebase_ModelConfiguration {
20
21     /**
22      * this holds (caches) the availability info of applications globally
23      * 
24      * @var array
25      */
26     static protected $_availableApplications = array('Tinebase' => TRUE);
27
28     /**
29      * the id property
30      *
31      * @var string
32      */
33     protected $_idProperty = 'id';
34     
35     // legacy
36     protected $_identifier;
37
38     /**
39      * Human readable name of the record
40      * add plural translation information in comments like:
41      * // ngettext('Record Name', 'Records Name', 1);
42      *
43      * @var string
44      */
45     protected $_recordName = NULL;
46
47     /**
48      * Human readable name of multiple records
49      * add plural translation information in comments like:
50      * // ngettext('Record Name', 'Records Name', 2);
51      *
52      * @var string
53      */
54     protected $_recordsName = NULL;
55
56     /**
57      * The property of the container, if any
58      *
59      * @var string
60      */
61     protected $_containerProperty = NULL;
62     
63     /**
64      * set this to false, if no filter and grid column should be created
65      * 
66      * @var boolean
67      */
68     protected $_containerUsesFilter = TRUE;
69     
70     /**
71      * The property of the title, if any
72      *
73      * if an array is given, the second item is the array of arguments for vsprintf, the first the format string
74      *
75      * @var string/array
76      */
77     protected $_titleProperty = 'title';
78
79
80     /**
81      * If this is true, the json api (smd) is generated automatically
82      *
83      * @var boolean
84      */
85     protected $_exposeJsonApi = NULL;
86     
87     /**
88      * Human readable name of the container
89      * add plural translation information in comments like:
90      * // ngettext('Record Name', 'Records Name', 2);
91      *
92      * @var string
93      */
94     protected $_containerName = NULL;
95
96     /**
97      * Human readable name of multiple containers
98      * add plural translation information in comments like:
99      * // ngettext('Record Name', 'Records Name', 2);
100      *
101      * @var string
102      */
103     protected $_containersName = NULL;
104
105     /**
106      * If this is true, the record has relations
107      *
108      * @var boolean
109      */
110     protected $_hasRelations = NULL;
111
112     /**
113      * If this is true, the record has customfields
114      *
115      * @var boolean
116      */
117     protected $_hasCustomFields = NULL;
118
119     /**
120      * If this is true, the record has notes
121      *
122      * @var boolean
123      */
124     protected $_hasNotes = NULL;
125
126     /**
127      * If this is true, the record has tags
128      *
129      * @var boolean
130      */
131     protected $_hasTags = NULL;
132
133     /**
134      * If this is true, the record has file attachments
135      *
136      * @var boolean
137      */
138     protected $_hasAttachments = NULL;
139     
140     /**
141      * If this is true, a modlog will be created
142      *
143      * @var boolean
144      */
145     protected $_modlogActive = NULL;
146
147     /**
148      * If this is true, multiple edit of records of this model is possible.
149      *
150      * @var boolean
151      */
152     protected $_multipleEdit = NULL;
153
154     /**
155      * If multiple edit requires a special right, it's defined here
156      *
157      * @var string
158      */
159     protected $_multipleEditRequiredRight = NULL;
160
161     /**
162      * if this is set to true, this model will be added to the global add splitbutton
163      *
164      * @todo add this to a "frontend configuration"
165      * 
166      * @var boolen
167      */
168     protected $_splitButton = FALSE;
169     
170     /**
171      * Group name of this model (will create a parent node in the modulepanel with this name)
172      * add translation information in comments like: // _('Group')
173      *
174      * @var string
175      */
176     protected $_moduleGroup = NULL;
177
178     /**
179      * Set the default Filter (defaults to query)
180      *
181      * @var string
182      */
183     protected $_defaultFilter = 'query';
184
185     /**
186      * Set the default sort info for the gridpanel (Tine.widgets.grid.GridPanel.defaultSortInfo)
187      * set as array('field' => 'title', 'direction' => 'DESC')
188      *
189      * @var array
190      */
191     protected $_defaultSortInfo = NULL;
192
193     /**
194      * Defines the right to see this model
195      *
196      * @var string
197      */
198     protected $_requiredRight = NULL;
199
200     /**
201      * no containers
202      * 
203      * @var boolean
204      */
205     protected $_singularContainerMode = NULL;
206
207     /**
208      * Holds the field definitions in an associative array where the key
209      * corresponds to the db-table name. Possible definitions and their defaults:
210      *
211      * !! Get sure to have at least one default value set and added one field to the query filter !!
212      *
213      * - validators: Use Zend Input Filters to validate the values.
214      *       @type: Array, @default: array(Zend_Filter_Input::ALLOW_EMPTY => true)
215      *
216      * - label: The human readable label of the field. If this is set to null, this won't be shown in the auto FE Grid or EditDialog.
217      *       Add translation information in comments like: // _('Title')
218      *       @type: String, @default: NULL
219      *
220      * - default: The default value of the field.
221      *       Add translation information in comments like: // _('New Car')
222      *       @type: as defined (see DEFAULT MAPPING), @default: NULL
223      *
224      * - duplicateCheckGroup: All Fields having the same group will be combined for duplicate
225      *       check. If no group is given, no duplicate check will be done.
226      *       @type: String, @default: NULL
227      *
228      * - type: The type of the Value
229      *       @type: String, @default: "string"
230      *
231      * - specialType: Defines the type closer
232      *       @type: String, @default: NULL
233      *
234      * - filterDefinition: Overwrites the default filter used for this field
235      *       Definition knows all from Tinebase_Model_Filter_FilterGroup._filterModel
236      *       @type: Array, @default: array('filter' => 'Tinebase_Model_Filter_Text') or the default used for this type (see DEFAULT MAPPING)
237      * 
238      * - inputFilters: zend input filters to use for this field
239      *       @type: Array, use array(<InPutFilterClassName> => <constructorData>, ...)
240      * 
241      * - queryFilter: If this is set to true, this field will be used by the "query" filter
242      *       @type: Boolean, @default: NULL
243      *
244      * - duplicateOmit: Will neither be shown nor handled on duplicate resolving
245      *       @type: Boolean, @default: NULL
246      *
247      * - copyOmit: If this is set to true, the field won't be used on copy the record
248      *       @type: Boolean, @default: NULL
249      *
250      * - readOnly: If this is set to true, the field can't be updated and will be shown as readOnly in the frontend
251      *       @type: Boolean, @default: NULL
252      *
253      * - disabled: If this is set to true, the field can't be updated and will be shown as readOnly in the frontend
254      *       @type: Boolean, @default: NULL
255      *
256      * - group: Add this field to a group. Each group will be shown as a separate FieldSet of the
257      *       EditDialog and group in the DuplicateResolveGridPanel. If any field of this model
258      *       has a group set, FieldSets will be created and fields without a group set will be
259      *       added to a group with the same name as the RecordName.
260      *       Add translation information in comments like: // _('Group')
261      *       @type: String, @default: NULL
262      *
263      * - dateFormat: If type is a date, the format can be overwritten here
264      *       @type: String, @default: NULL or the default used for this type (see DEFAULT MAPPING)
265      *
266      * - shy: If this is set to true, the row for this field won't be shown in the grid, but can be activated
267      * 
268      * - sortable: If this is set to false, no sort by this field is possible in the gridpanel, defaults to true
269      * 
270      *   // TODO: generalize, currently only in ContractGridPanel, take it from there:
271      * - showInDetailsPanel: auto show in details panel if any is defined in the js gridpanel class
272      * 
273      * - useGlobalTranslation: if set, the global translation is used
274      * 
275      * DEFAULT MAPPING:
276      *
277      * Field-Type  specialType   Human-Type          SQL-Type JS-Type                       PHP-Type          PHP-Filter                  dateFormat    JS-FilterType
278      *
279      * date                      Date                datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Short
280      * datetime                  Date with time      datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Long
281      * time                      Time                datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Time
282      * string                    Text                varchar  string                        string            Tinebase_Model_Filter_Text
283      * text                      Text with lnbr.     text     string                        string            Tinebase_Model_Filter_Text
284      * boolean                   Boolean             boolean  bool                          bool              Tinebase_Model_Filter_Bool
285      * integer                   Integer             integer  integer                       int               Tinebase_Model_Filter_Int                 number
286      * integer     bytes         Bytes               integer  integer                       int               Tinebase_Model_Filter_Int
287      * integer     usMoney       Dollar in Cent      integer  integer                       int               Tinebase_Model_Filter_Int
288      * integer     euMoney       Euro in Cent        integer  integer                       int               Tinebase_Model_Filter_Int
289      * integer     seconds       Seconds             integer  integer                       int               Tinebase_Model_Filter_Int
290      * integer     minutes       Minutes             integer  integer                       int               Tinebase_Model_Filter_Int
291      * float                     Float               float    float                         float             Tinebase_Model_Filter_Int
292      * json                      Json String         text     string                        array             Tinebase_Model_Filter_Text
293      * container                 Container           string   Tine.Tinebase.Model.Container Tinebase_Model_Container                                    tine.widget.container.filtermodel
294      * tag tinebase.tag
295      * user                      User                string                                 Tinebase_Model_Filter_User
296      * virtual:
297      * 
298      * Field Type "virtual" has a config property which holds the field configuration.
299      * An additional property is "function". If this property is set, the given function
300      * will be called to resolve the field in Tinebase_Convert_Json.
301      * If an array with two values is given, the first value will be handled as a class,
302      * the second one would be handled as a statically callable method.
303      * if the array is an associative one with one key and one value, 
304      * the key will be used for the classname of a singleton (callable by getInstance),
305      * the value will be used as method name.
306      * 
307      * * record/foreign (legacy) 1:1 - Relation      text     Tine.<APP>.Model.<MODEL>      Tinebase_Record_Abstract  Tinebase_Model_Filter_ForeignId   Tine.widgets.grid.ForeignRecordFilter
308      * * records                 1:n - Relation      -        Array of Record.data Objects  Tinebase_Record_RecordSet -                                 -
309      * * relation                m:m - Relation      -        Tinebase.Model.Relation       Tinebase_Model_Relation   Tinebase_Model_Filter_Relation
310      * * keyfield                String              string   <as defined>                  string            Tinebase_Model_Filter_Text
311      *
312      * * Accepts additional parameter: 'config' => array with these keys:
313      *     - @string appName    (the name of the application of the referenced record/s)
314      *     - @string modelName  (the name of the model of the referenced record/s)
315      *
316      *   Config for 'record' and 'records' accepts also these keys: (optionally if record class name doesn't fit the convention, will be autogenerated, if not set)
317      *     - @string recordClassName 
318      *     - @string controllerClassName
319      *     
320      *   Config for 'records' accepts also these keys:
321      *     - @string refIdField (the field of the foreign record referencing the idProperty of the own record)
322      *     - @array  paging     (accepts the parameters as Tinebase_Model_Pagination does)
323      *     - @string filterClassName
324      *     - @array  addFilters define them as array like Tinebase_Model_Filter_FilterGroup
325      * 
326      * record accepts keys additionally
327      *     - @string isParent   set this to true if the field is the parent property of an dependent record. This field will be hidden in an edit dialog nested grid
328      *     
329      * records accepts keys additionally
330      *     - @string omitOnSearch set this to FALSE, if the field should be resolved on json-search (defaults to TRUE)
331      *     
332      * <code>
333      *
334      * array(
335      *     'title' => array(
336      *         'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
337      *         'label' => NULL,
338      *         'duplicateCheckGroup' => 'number'
339      *     ),
340      *     'number' => ...
341      * )
342      *
343      * </code>
344      *
345      * @var array
346      */
347     protected $_fields = array();
348     
349     /**
350      * if this is set to true, all virtual fields get resolved by the record controller method "resolveVirtualFields"
351      *
352      * @var boolean
353      */
354     protected $_resolveVFGlobally = FALSE;
355     
356     /**
357      * holds all field definitions of type records
358      *
359      * @var array
360     */
361     protected $_recordsFields = NULL;
362
363     /**
364      * holds all field definitions of type record (foreignId fields)
365      *
366      * @var array
367      */
368     protected $_recordFields  = NULL;
369
370     /**
371      * if this is set to true, related data will be fetched on fetching dependent records by frontend json
372      * look at: Tinebase_Convert_Json._resolveMultipleRecordFields
373      * 
374      * @var boolean
375      */
376     protected $_resolveRelated = FALSE;
377     
378     /**
379      * holds virtual field definitions used for non-persistent fields getting calculated on each call of the record
380      * no backend property will be build, no filters etc. will exist. they must be filled in frontend json
381      * 
382      * @var array
383      */
384     protected $_virtualFields = NULL;
385     
386     /**
387      * maps fieldgroup keys to their names
388      * Add translation information in comments like: // _('Banking Information')
389      * 
390      * array(
391      *     'banking' => 'Banking Information',    // _('Banking Information')
392      *     'private' => 'Private Information',    // _('Private Information')
393      *     )
394      * 
395      * @var array
396      */
397     protected $_fieldGroups = NULL;
398     
399     /**
400      * here you can define one right (Tinebase_Acl_Rights_Abstract) for each field
401      * group ('group'-property of a field definition of this._fields), the user must
402      * have to see/edit this group, otherwise the fields of the edit dialog will be disabled/readOnly
403      *
404      * array(
405      *     'private' => array(
406      *         'see'  => HumanResources_Acl_Rights::SEE_PRIVATE,
407      *         'edit' => HumanResources_Acl_Rights::EDIT_PRIVATE,
408      *     ),
409      *     'banking' => array(
410      *         'see'  => HumanResources_Acl_Rights::SEE_BANKING,
411      *         'edit' => HumanResources_Acl_Rights::EDIT_BANKING,
412      *     )
413      * );
414      *
415      * @var array
416      */
417     protected $_fieldGroupRights = array();
418
419     /**
420      * every field group will be nested into a fieldset, here you can define the defaults (Ext.Container.defaults)
421      *
422      * @var array
423     */
424     protected $_fieldGroupFeDefaults = array();
425
426     protected $_createModule = FALSE;
427     
428     /*
429      * auto set by the constructor
430     */
431
432     /**
433      * If any field has a group, this will be set to true (autoset by the constructor)
434      *
435      * @var boolean
436     */
437     protected $_useGroups = FALSE;
438
439     /**
440      * the application this configuration belongs to (if the class has the name "Calendar_Model_Event", this will be resolved to "Calendar")
441      *
442      * @var string
443      */
444     protected $_appName = NULL;    // this should be used everytime, everywhere
445     // legacy
446     protected $_application = NULL;
447     protected $_applicationName = NULL;
448     /**
449      * the name of the model (if the class has the name "Calendar_Model_Event", this will be resolved to "Event")
450      *
451      * @var string
452      */
453     protected $_modelName = NULL;
454
455     /**
456      * holds the keys of all fields
457      *
458      * @var array
459      */
460     protected $_fieldKeys = array();
461
462     /**
463      * holds the time fields
464      *
465      * @var array
466     */
467     protected $_timeFields = array();
468
469     /**
470      * holds the fields which will be omitted in the modlog
471      *
472      * @var array
473     */
474     protected $_modlogOmitFields = array();
475
476     /**
477      * these fields will just be readOnly
478      *
479      * @var array
480     */
481     protected $_readOnlyFields = array();
482
483     /**
484      * holds the datetime fields
485      *
486      * @var array
487     */
488     protected $_datetimeFields = array();
489
490     /**
491      * holds the date fields (maybe we use Tinebase_Date sometimes)
492      * 
493      * @var array
494      */
495     protected $_dateFields = array();
496     
497     /**
498      * holds the alarm datetime fields
499      *
500      * @var array
501     */
502     protected $_alarmDateTimeField = array();
503
504     /**
505      * The calculated filters for this model (auto set)
506      *
507      * @var array
508     */
509     protected $_filterModel = array();
510
511     /**
512      * holds the validators for the model (auto set)
513     */
514     protected $_validators = array();
515
516     /**
517      * holds validators which will be instanciated on model construction
518      *
519      * @var array
520     */
521     protected $_ownValidators = array();
522     
523     /**
524      * if a record is dependent to another, this is true
525      * 
526      * @var boolean
527      */
528     protected $_isDependent = FALSE;
529     
530     /**
531      * input filters (will be set by field configuration)
532      * 
533      * @var array
534      */
535     protected $_filters;
536
537     /**
538      * converters (will be set by field configuration)
539      *
540      * @var array
541      */
542     protected $_converters = array();
543     
544     /**
545      * Holds the default Data for the model (autoset from field config)
546      *
547      * @var array
548     */
549     protected $_defaultData = array();
550
551     /**
552      * holds the fields / groups to check for duplicates (will be auto set by field configuration)
553     */
554     protected $_duplicateCheckFields = NULL;
555
556     /**
557      * properties to collect for the filters (_appName and _modelName are set in the filter)
558      *
559      * @var array
560      */
561     protected $_filterProperties = array('_filterModel', '_defaultFilter', '_modelName', '_applicationName');
562
563     /**
564      * properties to collect for the model
565      *
566      * @var array
567     */
568     protected $_modelProperties = array(
569         '_identifier', '_timeFields', '_dateFields', '_datetimeFields', '_alarmDateTimeField', '_validators', '_modlogOmitFields',
570         '_application', '_readOnlyFields', '_filters'
571     );
572
573     /**
574      * properties to collect for the frontend
575      *
576      * @var array
577     */
578     protected $_frontendProperties = array(
579         'containerProperty', 'containersName', 'containerName', 'defaultSortInfo', 'fieldKeys', 'filterModel',
580         'defaultFilter', 'requiredRight', 'singularContainerMode', 'fields', 'defaultData', 'titleProperty',
581         'useGroups', 'fieldGroupFeDefaults', 'fieldGroupRights', 'multipleEdit', 'multipleEditRequiredRight',
582         'recordName', 'recordsName', 'appName', 'modelName', 'createModule', 'virtualFields', 'group', 'isDependent',
583         'hasCustomFields', 'modlogActive', 'hasAttachments', 'idProperty', 'splitButton', 'attributeConfig'
584     );
585
586     /**
587      * the module group (will be on the same leaf of the content type tree panel)
588      * 
589      * @var string
590      */
591     protected $_group = NULL;
592     
593     /**
594      * the backend properties holding the collected properties
595      *
596      * @var array
597     */
598     protected $_modelConfiguration = NULL;
599
600     /**
601      * holds the collected values for the frontendconfig (autoset on first call of getFrontendConfiguration)
602      * 
603      * @var array
604      */
605     protected $_frontendConfiguration = NULL;
606     
607     /**
608      * the backend properties holding the collected properties
609      *
610      * @var array
611      */
612     protected $_filterConfiguration = NULL;
613
614     /**
615      *
616      * @var array
617      */
618     protected $_attributeConfig = NULL;
619
620     /*
621      * mappings
622      */
623
624     /**
625      * This defines the filters use for all known types
626      * @var array
627      */
628     protected $_filterModelMapping = array(
629         'date'     => 'Tinebase_Model_Filter_Date',
630         'datetime' => 'Tinebase_Model_Filter_DateTime',
631         'time'     => 'Tinebase_Model_Filter_Date',
632         'string'   => 'Tinebase_Model_Filter_Text',
633         'text'     => 'Tinebase_Model_Filter_Text',
634         'json'     => 'Tinebase_Model_Filter_Text',
635         'boolean'  => 'Tinebase_Model_Filter_Bool',
636         'integer'  => 'Tinebase_Model_Filter_Int',
637         'float'    => 'Tinebase_Model_Filter_Float',
638         'record'   => 'Tinebase_Model_Filter_ForeignId',
639         'relation' => 'Tinebase_Model_Filter_Relation',
640
641         'keyfield'  => 'Tinebase_Model_Filter_Text',
642         'container' => 'Tinebase_Model_Filter_Container',
643         'tag'       => 'Tinebase_Model_Filter_Tag',
644         'user'      => 'Tinebase_Model_Filter_User',
645     );
646
647     /**
648      * This maps field types to own validators, which will be instanciated in the constructor.
649      *
650      * @var array
651     */
652     protected $_inputFilterDefaultMapping = array(
653         'text'     => array('Tinebase_Model_InputFilter_CrlfConvert'),
654     );
655
656     /**
657      * This maps field types to their default validators, just zendfw validators can be used here.
658      * For using own validators, use _ownValidatorMapping instead. If no validator is given,
659      * "array(Zend_Filter_Input::ALLOW_EMPTY => true)" will be used
660      *
661      * @var array
662     */
663     protected $_validatorMapping = array(
664         'record'    => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
665         'relation'  => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
666     );
667
668     /**
669      * This maps field types to their default converter
670      *
671      * @var array
672      */
673     protected $_converterDefaultMapping = array(
674         'json'      => array('Tinebase_Model_Converter_Json'),
675     );
676
677     /**
678      * the constructor (must be called by the singleton pattern)
679      *
680      * @var array $modelClassConfiguration
681      * @throws Tinebase_Exception_Record_DefinitionFailure
682      */
683     public function __construct($modelClassConfiguration)
684     {
685         if (! $modelClassConfiguration) {
686             throw new Tinebase_Exception('The model class configuration must be submitted!');
687         }
688
689         $this->_appName     = $this->_application = $this->_applicationName = $modelClassConfiguration['appName'];
690         
691         // add appName to available applications 
692         self::$_availableApplications[$this->_appName] = TRUE;
693         
694         $this->_modelName   = $modelClassConfiguration['modelName'];
695         $this->_idProperty  = $this->_identifier = (isset($modelClassConfiguration['idProperty']) || array_key_exists('idProperty', $modelClassConfiguration)) ? $modelClassConfiguration['idProperty'] : 'id';
696
697         // some cruid validating
698         foreach ($modelClassConfiguration as $propertyName => $propertyValue) {
699             $this->{'_' . $propertyName} = $propertyValue;
700         }
701         
702         $this->_filters = array();
703         $this->_fields[$this->_idProperty] = array('label' => NULL, 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true));
704
705         if ($this->_hasCustomFields) {
706             $this->_fields['customfields'] = array('label' => NULL, 'type' => 'custom', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
707         }
708
709         if ($this->_hasRelations) {
710             $this->_fields['relations'] = array('label' => NULL, 'type' => 'relation', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
711         }
712
713         if ($this->_containerProperty) {
714             $this->_fields[$this->_containerProperty] = array(
715                 'label'            => $this->_containerUsesFilter ? $this->_containerName : NULL,
716                 'shy'              => true,
717                 'type'             => 'container',
718                 'validators'       => array(Zend_Filter_Input::ALLOW_EMPTY => true),
719                 'filterDefinition' => array(
720                     'filter'  => $this->_filterModelMapping['container'],
721                     'options' => array('applicationName' => $this->_appName)
722                 )
723             );
724         } else {
725             $this->_singularContainerMode = true;
726         }
727
728         // quick hack ('key')
729         if ($this->_hasTags) {
730             $this->_fields['tags'] = array(
731                 'label' => 'Tags',
732                 'sortable' => false,
733                 'type' => 'tag', 
734                 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL), 
735                 'useGlobalTranslation' => TRUE,
736                 'filterDefinition' => array(
737                     'key'     => 'tag',
738                     'filter'  => $this->_filterModelMapping['tag'],
739                     'options' => array(
740                            'idProperty' => $this->_idProperty,
741                            'applicationName' => $this->_appName
742                     )
743                 )
744             );
745         }
746
747         if ($this->_hasAttachments) {
748             $this->_fields['attachments'] = array(
749                 'label' => NULL,
750                 'type'  => 'attachments'
751             );
752         }
753         
754         
755         if ($this->_modlogActive) {
756             // notes are needed if modlog is active
757             $this->_fields['notes']              = array('label' => NULL,                 'type' => 'note',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL), 'useGlobalTranslation' => TRUE);
758             $this->_fields['created_by']         = array('label' => 'Created By',         'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
759             $this->_fields['creation_time']      = array('label' => 'Creation Time',      'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
760             $this->_fields['last_modified_by']   = array('label' => 'Last Modified By',   'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
761             $this->_fields['last_modified_time'] = array('label' => 'Last Modified Time', 'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
762             $this->_fields['seq']                = array('label' => NULL,                 'type' => 'integer',  'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
763             
764             // don't show deleted information
765             $this->_fields['deleted_by']         = array('label' => NULL, 'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
766             $this->_fields['deleted_time']       = array('label' => NULL, 'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
767             $this->_fields['is_deleted']         = array('label' => NULL, 'type' => 'boolean',  'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
768
769         } elseif ($this->_hasNotes) {
770             $this->_fields['notes'] = array('label' => NULL, 'type' => 'note', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
771         }
772         
773         // holds the filters used for the query-filter, if any
774         $queryFilters = array();
775         
776         foreach ($this->_fields as $fieldKey => &$fieldDef) {
777             // set default type to string, if no type is given
778             if (! (isset($fieldDef['type']) || array_key_exists('type', $fieldDef))) {
779                 $fieldDef['type'] = 'string';
780             }
781             
782             // don't handle field if app is not available
783             if ((isset($fieldDef['config']) || array_key_exists('config', $fieldDef)) && ($fieldDef['type'] == 'record' || $fieldDef['type'] == 'records') && (! $this->_isAvailable($fieldDef['config']))) {
784                 $fieldDef['type'] = 'string';
785                 $fieldDef['label'] = NULL;
786                 continue;
787             }
788             // the property name
789             $fieldDef['key'] = $fieldKey;
790
791             // if any field has a group set, enable grouping globally
792             if (! $this->_useGroups && (isset($fieldDef['group']) || array_key_exists('group', $fieldDef))) {
793                 $this->_useGroups = TRUE;
794             }
795
796             if ($fieldDef['type'] == 'virtual') {
797                 $fieldDef = isset($fieldDef['config']) ? $fieldDef['config'] : array();
798                 $fieldDef['key'] = $fieldKey;
799                 $fieldDef['sortable'] = FALSE;
800                 if ((isset($fieldDef['default']))) {
801                     // @todo: better handling of virtualfields
802                     $this->_defaultData[$fieldKey] = $fieldDef['default'];
803                 }
804                 $this->_virtualFields[] = $fieldDef;
805                 continue;
806             }
807             
808             // set default value
809             // TODO: implement complex default values
810             if ((isset($fieldDef['default']))) {
811 //                 // allows dynamic default values
812 //                 if (is_array($fieldDef['default'])) {
813 //                     switch ($fieldDef['type']) {
814 //                         case 'time':
815 //                         case 'date':
816 //                         case 'datetime':
817 //                         default:
818 //                             throw new Tinebase_Exception_NotImplemented($_message);
819 //                     }
820 //                 } else {
821                     $this->_defaultData[$fieldKey] = $fieldDef['default'];
822                     
823 //                 }
824             }
825
826             // TODO: Split this up in multiple functions
827             // TODO: Refactor: key 'tag' should be 'tags' in filter definition / quick hack
828             // also see ticket 8944 (https://forge.tine20.org/mantisbt/view.php?id=8944)
829             
830             // set filter model
831             if ((isset($fieldDef['filterDefinition']) || array_key_exists('filterDefinition', $fieldDef))) {
832                 // use filter from definition
833                 $key = isset($fieldDef['filterDefinition']['key']) ? $fieldDef['filterDefinition']['key'] : $fieldKey;
834                 $this->_filterModel[$key] = $fieldDef['filterDefinition'];
835             } else if ((isset($this->_filterModelMapping[$fieldDef['type']]) || array_key_exists($fieldDef['type'], $this->_filterModelMapping))) {
836                 // if no filterDefinition is given, try to use the default one
837                 $this->_filterModel[$fieldKey] = array('filter' => $this->_filterModelMapping[$fieldDef['type']]);
838                 if ((isset($fieldDef['config']) || array_key_exists('config', $fieldDef))) {
839                     $this->_filterModel[$fieldKey]['options'] = $fieldDef['config'];
840                     
841                     // set id filter controller
842                     if ($fieldDef['type'] == 'record') {
843                         $this->_filterModel[$fieldKey]['options']['filtergroup'] = $fieldDef['config']['appName'] . '_Model_' . $fieldDef['config']['modelName'] . 'Filter';
844                         $this->_filterModel[$fieldKey]['options']['controller']  = $fieldDef['config']['appName'] . '_Controller_' . $fieldDef['config']['modelName'];
845                     }
846                 }
847             }
848             
849             if ((isset($fieldDef['queryFilter']) || array_key_exists('queryFilter', $fieldDef))) {
850                 $queryFilters[] = $fieldKey;
851             }
852
853             // set validators
854             if ((isset($fieldDef['validators']) || array_key_exists('validators', $fieldDef))) {
855                 // use _validators from definition
856                 $this->_validators[$fieldKey] = $fieldDef['validators'];
857             } else if ((isset($this->_validatorMapping[$fieldDef['type']]) || array_key_exists($fieldDef['type'], $this->_validatorMapping))) {
858                 // if no validatorsDefinition is given, try to use the default one
859                 $fieldDef['validators'] = $this->_validators[$fieldKey] = $this->_validatorMapping[$fieldDef['type']];
860             } else {
861                 $fieldDef['validators'] = $this->_validators[$fieldKey] = array(Zend_Filter_Input::ALLOW_EMPTY => true);
862             }
863             
864             // set input filters, append defined if any or use defaults from _inputFilterDefaultMapping 
865             if ((isset($fieldDef['inputFilters']) || array_key_exists('inputFilters', $fieldDef))) {
866                 foreach ($fieldDef['inputFilters'] as $if => $val) {
867                     if (is_array($val)) {
868                         $reflect  = new ReflectionClass($if);
869                         $this->_filters[$fieldKey][] = $reflect->newInstanceArgs($val);
870                     } else {
871                         $this->_filters[$fieldKey][] = $if ? new $if($val) : new $val();
872                     }
873                 }
874             } else if ((isset($this->_inputFilterDefaultMapping[$fieldDef['type']]) || array_key_exists($fieldDef['type'], $this->_inputFilterDefaultMapping))) {
875                 foreach ($this->_inputFilterDefaultMapping[$fieldDef['type']] as $if => $val) {
876                     $this->_filters[$fieldKey][] = $if ? new $if($val) : new $val();
877                 }
878             }
879             
880             // add field to modlog omit, if configured and modlog is used
881             if ($this->_modlogActive && (isset($fieldDef['modlogOmit']) || array_key_exists('modlogOmit', $fieldDef))) {
882                 $this->_modlogOmitFields[] = $fieldKey;
883             }
884
885             // set converters
886             if (isset($fieldDef['converters']) && is_array($fieldDef['converters'])) {
887                 if (count($fieldDef['converters'])) {
888                     $this->_converters[$fieldKey] = $fieldDef['converters'];
889                 }
890             } elseif(isset($this->_converterDefaultMapping[$fieldDef['type']])) {
891                 $this->_converters[$fieldKey] = $this->_converterDefaultMapping[$fieldDef['type']];
892             }
893             
894             $this->_populateProperties($fieldKey, $fieldDef);
895             
896         }
897         
898         // set some default filters
899         if (count($queryFilters)) {
900             $this->_getQueryFilter($queryFilters);
901         }
902         $this->_filterModel[$this->_idProperty] = array('filter' => 'Tinebase_Model_Filter_Id', 'options' => array('idProperty' => $this->_idProperty, 'modelName' => $this->_appName . '_Model_' . $this->_modelName));
903         $this->_fieldKeys = array_keys($this->_fields);
904     }
905
906     /**
907      * constructs the query filter
908      *
909      * adds ExplicitRelatedRecords-filters to query filter (relatedModels) to allow search in relations
910      *
911      * @param array $queryFilters
912      *
913      * @see 0011494: activate advanced search for contracts (customers, ...)
914      */
915     protected function _getQueryFilter($queryFilters)
916     {
917         $queryFilterData = array(
918             'label' => 'Quick Search',
919             'field' => 'query',
920             'filter' => 'Tinebase_Model_Filter_Query',
921             'useGlobalTranslation' => true,
922             'options' => array(
923                 'fields' => $queryFilters,
924                 'modelName' => $this->_getPhpClassName(),
925             )
926         );
927
928         $relatedModels = array();
929         foreach ($this->_filterModel as $name => $filter) {
930             if ($filter['filter'] === 'Tinebase_Model_Filter_ExplicitRelatedRecord') {
931                 $relatedModels[] = $filter['options']['related_model'];
932             }
933         }
934         if (count($relatedModels) > 0) {
935             $queryFilterData['options']['relatedModels'] = array_unique($relatedModels);
936         }
937
938         $this->_filterModel['query'] = $queryFilterData;
939     }
940
941     /**
942      * populate model config properties
943      * 
944      * @param string $fieldKey
945      * @param array $fieldDef
946      */
947     protected function _populateProperties($fieldKey, $fieldDef)
948     {
949         switch ($fieldDef['type']) {
950             case 'string':
951             case 'text':
952             case 'integer':
953             case 'float':
954             case 'boolean':
955                 break;
956             case 'container':
957                 break;
958             case 'date':
959                 // add to datetime fields
960                 $this->_dateFields[] = $fieldKey;
961                 break;
962             case 'datetime':
963                 // add to alarm fields
964                 if ((isset($fieldDef['alarm']) || array_key_exists('alarm', $fieldDef))) {
965                     $this->_alarmDateTimeField = $fieldKey;
966                 }
967                 // add to datetime fields
968                 $this->_datetimeFields[] = $fieldKey;
969                 break;
970             case 'time':
971                 // add to timefields
972                 $this->_timeFields[] = $fieldKey;
973                 break;
974             case 'user':
975                 $fieldDef['config'] = array(
976                     'refIdField'              => 'id',
977                     'appName'                 => 'Addressbook',
978                     'modelName'               => 'Contact',
979                     'recordClassName'         => 'Addressbook_Model_Contact',
980                     'controllerClassName'     => 'Addressbook_Controller_Contact',
981                     'filterClassName'         => 'Addressbook_Model_ContactFilter',
982                     'addFilters' => array(
983                         array('field' => 'type', 'operator' => 'equals', 'value' => 'user')
984                     )
985                 );
986                 $this->_recordFields[$fieldKey] = $fieldDef;
987                 break;
988             case 'record':
989                 $this->_filterModel[$fieldKey]['options']['controller']  = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Controller');
990                 $this->_filterModel[$fieldKey]['options']['filtergroup'] = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Model') . 'Filter';
991             case 'records':
992                 $fieldDef['config']['recordClassName']     = (isset($fieldDef['config']['recordClassName']) || array_key_exists('recordClassName', $fieldDef['config']))     ? $fieldDef['config']['recordClassName']     : $this->_getPhpClassName($fieldDef['config']);
993                 $fieldDef['config']['controllerClassName'] = (isset($fieldDef['config']['controllerClassName']) || array_key_exists('controllerClassName', $fieldDef['config'])) ? $fieldDef['config']['controllerClassName'] : $this->_getPhpClassName($fieldDef['config'], 'Controller');
994                 $fieldDef['config']['filterClassName']     = (isset($fieldDef['config']['filterClassName']) || array_key_exists('filterClassName', $fieldDef['config']))     ? $fieldDef['config']['filterClassName']     : $this->_getPhpClassName($fieldDef['config']) . 'Filter';
995                 if ($fieldDef['type'] == 'record') {
996                     $this->_recordFields[$fieldKey] = $fieldDef;
997                 } else {
998                     $fieldDef['config']['dependentRecords'] = (isset($fieldDef['config']['dependentRecords']) || array_key_exists('dependentRecords', $fieldDef['config'])) ? $fieldDef['config']['dependentRecords'] : FALSE;
999                     $this->_recordsFields[$fieldKey] = $fieldDef;
1000                 }
1001                 break;
1002             case 'custom':
1003                 try {
1004                     // prepend table name to id prop because of ambiguous ids
1005                     // TODO find a better way to get table name, maybe we should put it in the modelconfig?
1006                     $backend = Tinebase_Core::getApplicationInstance($this->_applicationName, $this->_modelName)->getBackend();
1007                     $tableName = $backend->getTableName();
1008                     $this->_filterModel['customfield'] = array(
1009                         'filter' => 'Tinebase_Model_Filter_CustomField', 
1010                         'options' => array(
1011                             'idProperty' => $tableName . '.' . $this->_idProperty
1012                         )
1013                     );
1014                 } catch (Exception $e) {
1015                     // no customfield filter available (yet?)
1016                     Tinebase_Exception::log($e);
1017                 }
1018                 break;
1019             default:
1020                 break;
1021         }
1022     }
1023     
1024     /**
1025      * returns an instance of the record controller
1026      * 
1027      * @return Tinebase_Controller_Record_Interface
1028      */
1029     public function getControllerInstance()
1030     {
1031         return Tinebase_Core::getApplicationInstance($this->_appName, $this->_modelName);
1032     }
1033     
1034     /**
1035      * gets phpClassName by field definition['config']
1036      *
1037      * @param array $_fieldConfig
1038      * @param string $_type
1039      * @return string
1040      */
1041     protected function _getPhpClassName($_fieldConfig = null, $_type = 'Model')
1042     {
1043         if (! $_fieldConfig) {
1044             $_fieldConfig = array('appName' => $this->_appName, 'modelName' => $this->_modelName);
1045         }
1046
1047         return $_fieldConfig['appName'] . '_' . $_type . '_' . $_fieldConfig['modelName'];
1048     }
1049     
1050     /**
1051      * checks if app and model is available for the user at record and records fields
1052      * later this can be used to use field acl
1053      * 
1054      * @param array $_fieldConfig the field configuration
1055      */
1056     protected function _isAvailable($_fieldConfig)
1057     {
1058         if (! (isset(self::$_availableApplications[$_fieldConfig['appName']]) || array_key_exists($_fieldConfig['appName'], self::$_availableApplications))) {
1059             self::$_availableApplications[$_fieldConfig['appName']] = Tinebase_Application::getInstance()->isInstalled($_fieldConfig['appName'], TRUE);
1060         }
1061         return self::$_availableApplications[$_fieldConfig['appName']];
1062     }
1063     
1064     /**
1065      * returns the filterconfiguration needed in the filtergroup for this model
1066      * 
1067      * @return array
1068      */
1069     public function getFilterModel()
1070     {
1071         // add calculated values to filter configuration
1072         if (! $this->_filterConfiguration) {
1073             foreach ($this->_filterProperties as $prop) {
1074                 $this->_filterConfiguration[$prop] = $this->{$prop};
1075             }
1076             // @todo: remove this as in the filtergroup
1077             $this->_filterConfiguration['_className'] = $this->_appName . '_Model_' . $this->_modelName . 'Filter';
1078         }
1079         return $this->_filterConfiguration;
1080     }
1081
1082     /**
1083      * returns the properties needed for the model
1084      * 
1085      * @return array
1086      */
1087     public function toArray()
1088     {
1089         if (! $this->_modelConfiguration) {
1090             // add calculated values to model configuration
1091             foreach ($this->_modelProperties as $prop) {
1092                 $this->_modelConfiguration[$prop] = $this->{$prop};
1093             }
1094         }
1095         
1096         return $this->_modelConfiguration;
1097     }
1098
1099     /**
1100      * returns the frontend configuration for creating js models, filters, defaults and some other js stubs.
1101      * this will be included in the registry.
1102      * Look at Tinebase/js/ApplicationStarter.js
1103      */
1104     public function getFrontendConfiguration()
1105     {
1106         if (! $this->_frontendConfiguration) {
1107             
1108             $this->_frontendConfiguration = array();
1109             
1110             // add calculated values to frontend configuration
1111             foreach ($this->_frontendProperties as $prop) {
1112                 $this->_frontendConfiguration[$prop] = $this->{'_' . $prop};
1113             }
1114         }
1115         return $this->_frontendConfiguration;
1116     }
1117
1118     /**
1119      * returns default data for this model
1120      * 
1121      * @return array
1122      */
1123     public function getDefaultData()
1124     {
1125         return $this->_defaultData;
1126     }
1127
1128     /**
1129      * returns the field configuration of the model
1130      */
1131     public function getFields()
1132     {
1133         return $this->_fields;
1134     }
1135
1136     /**
1137      * returns the converters of the model
1138      */
1139     public function getConverters()
1140     {
1141         return $this->_converters;
1142     }
1143
1144     /**
1145      * get protected property
1146      *
1147      * @param string name of property
1148      * @throws Tinebase_Exception_UnexpectedValue
1149      * @return mixed value of property
1150      */
1151     public function __get($_property)
1152     {
1153         if (! property_exists($this,  '_' . $_property)) {
1154             throw new Tinebase_Exception_UnexpectedValue('Property does not exist: ' . $_property);
1155         }
1156         return $this->{'_' . $_property};
1157     }
1158 }