#8212: can't sort by tags
[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 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 availibility 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      * The property of the title, if any
65      *
66      * @var string
67      */
68     protected $_titleProperty = 'title';
69     
70     /**
71      * Human readable name of the container
72      * add plural translation information in comments like:
73      * // ngettext('Record Name', 'Records Name', 2);
74      *
75      * @var string
76      */
77     protected $_containerName = NULL;
78
79     /**
80      * Human readable name of multiple containers
81      * add plural translation information in comments like:
82      * // ngettext('Record Name', 'Records Name', 2);
83      *
84      * @var string
85      */
86     protected $_containersName = NULL;
87
88     /**
89      * If this is true, the record has relations
90      *
91      * @var boolean
92      */
93     protected $_hasRelations = NULL;
94
95     /**
96      * If this is true, the record has customfields
97      *
98      * @var boolean
99      */
100     protected $_hasCustomFields = NULL;
101
102     /**
103      * If this is true, the record has notes
104      *
105      * @var boolean
106      */
107     protected $_hasNotes = NULL;
108
109     /**
110      * If this is true, the record has tags
111      *
112      * @var boolean
113      */
114     protected $_hasTags = NULL;
115
116     /**
117      * If this is true, a modlog will be created
118      *
119      * @var boolean
120      */
121     protected $_modlogActive = NULL;
122
123     /**
124      * If this is true, multiple edit of records of this model is possible.
125      *
126      * @var boolean
127      */
128     protected $_multipleEdit = NULL;
129
130     /**
131      * If multiple edit requires a special right, it's defined here
132      *
133      * @var string
134      */
135     protected $_multipleEditRequiredRight = NULL;
136
137     /**
138      * Group name of this model (will create a parent node in the modulepanel with this name)
139      * add translation information in comments like: // _('Group')
140      *
141      * @var string
142      */
143     protected $_moduleGroup = NULL;
144
145     /**
146      * Set the default Filter (defaults to query)
147      *
148      * @var string
149      */
150     protected $_defaultFilter = 'query';
151
152     /**
153      * Set the default sort info for the gridpanel (Tine.widgets.grid.GridPanel.defaultSortInfo)
154      * set as array('field' => 'title', 'direction' => 'DESC')
155      *
156      * @var array
157      */
158     protected $_defaultSortInfo = NULL;
159
160     /**
161      * Defines the right to see this model
162      *
163      * @var string
164      */
165     protected $_requiredRight = NULL;
166
167     /**
168      * no containers
169      * 
170      * @var boolean
171      */
172     protected $_singularContainerMode = NULL;
173
174     /**
175      * Holds the field definitions in an associative array where the key
176      * corresponds to the db-table name. Possible definitions and their defaults:
177      *
178      * - validators: Use Zend Input Filters to validate the values.
179      *       @type: Array, @default: array(Zend_Filter_Input::ALLOW_EMPTY => true)
180      *
181      * - 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.
182      *       Add translation information in comments like: // _('Title')
183      *       @type: String, @default: NULL
184      *
185      * - default: The default Value of the field.
186      *       Add translation information in comments like: // _('New Car')
187      *       @type: as defined (see DEFAULT MAPPING), @default: NULL
188      *
189      * - duplicateCheckGroup: All Fields having the same group will be combined for duplicate
190      *       check. If no group is given, no duplicate check will be done.
191      *       @type: String, @default: NULL
192      *
193      * - type: The type of the Value
194      *       @type: String, @default: "string"
195      *
196      * - specialType: Defines the type closer
197      *       @type: String, @default: NULL
198      *
199      * - filterDefinition: Overwrites the default filter used for this field
200      *       Definition knows all from Tinebase_Model_Filter_FilterGroup._filterModel
201      *       @type: Array, @default: array('filter' => 'Tinebase_Model_Filter_Text') or the default used for this type (see DEFAULT MAPPING)
202      * 
203      * - inputFilters: zend input filters to use for this field
204      *       @type: Array, use array(<InPutFilterClassName> => <constructorData>, ...)
205      * 
206      * - queryFilter: If this is set to true, this field will be used by the "query" filter
207      *       @type: Boolean, @default: NULL
208      *
209      * - duplicateOmit: Will neither be shown nor handled on duplicate resolving
210      *       @type: Boolean, @default: NULL
211      *
212      * - copyOmit: If this is set to true, the field won't be used on copy the record
213      *       @type: Boolean, @default: NULL
214      *
215      * - readOnly: If this is set to true, the field can't be updated and will be shown as readOnly in the frontend
216      *       @type: Boolean, @default: NULL
217      *
218      * - disabled: If this is set to true, the field can't be updated and will be shown as readOnly in the frontend
219      *       @type: Boolean, @default: NULL
220      *
221      * - group: Add this field to a group. Each group will be shown as a separate FieldSet of the
222      *       EditDialog and group in the DuplicateResolveGridPanel. If any field of this model
223      *       has a group set, FieldSets will be created and fields without a group set will be
224      *       added to a group with the same name as the RecordName.
225      *       Add translation information in comments like: // _('Group')
226      *       @type: String, @default: NULL
227      *
228      * - dateFormat: If type is a date, the format can be overwritten here
229      *       @type: String, @default: NULL or the default used for this type (see DEFAULT MAPPING)
230      *
231      * - shy: If this is set to true, the row for this field won't be shown in the grid, but can be activated
232      * 
233      * - sortable: If this is set to false, no sort by this field is possible in the gridpanel, defaults to true
234      * 
235      *   // TODO: generalize, currently only in ContractGridPanel, take it from there:
236      * - showInDetailsPanel: auto show in details panel if any is defined in the js gridpanel class
237      * 
238      * - useGlobalTranslation: if set, the global translation is used
239      * 
240      * DEFAULT MAPPING:
241      *
242      * Field-Type  specialType   Human-Type          SQL-Type JS-Type                       PHP-Type          PHP-Filter                  dateFormat    JS-FilterType
243      *
244      * date                      Date                datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Short
245      * datetime                  Date with time      datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Long
246      * time                      Time                datetime date                          Tinebase_DateTime Tinebase_Model_Filter_Date  ISO8601Time
247      * string                    Text                varchar  string                        string            Tinebase_Model_Filter_Text
248      * text                      Text with lnbr.     text     string                        string            Tinebase_Model_Filter_Text
249      * boolean                   Boolean             boolean  bool                          bool              Tinebase_Model_Filter_Bool
250      * integer                   Integer             integer  integer                       int               Tinebase_Model_Filter_Int                 number
251      * integer     bytes         Bytes               integer  integer                       int               Tinebase_Model_Filter_Int
252      * integer     usMoney       Dollar in Cent      integer  integer                       int               Tinebase_Model_Filter_Int
253      * integer     euMoney       Euro in Cent        integer  integer                       int               Tinebase_Model_Filter_Int
254      * integer     seconds       Seconds             integer  integer                       int               Tinebase_Model_Filter_Int
255      * integer     minutes       Minutes             integer  integer                       int               Tinebase_Model_Filter_Int
256      * float                     Float               float    float                         float             Tinebase_Model_Filter_Int
257      * container                 Container           string   Tine.Tinebase.Model.Container Tinebase_Model_Container                                    tine.widget.container.filtermodel
258      * tag tinebase.tag
259      * user                      User                string                                 Tinebase_Model_Filter_User
260      *
261      * * record/foreign (legacy) 1:1 - Relation      text     Tine.<APP>.Model.<MODEL>      Tinebase_Record_Abstract  Tinebase_Model_Filter_ForeignId   Tine.widgets.grid.ForeignRecordFilter
262      * * records                 1:n - Relation      -        Array of Record.data Objects  Tinebase_Record_RecordSet -                                 -
263      * * relation                m:m - Relation      -        Tinebase.Model.Relation       Tinebase_Model_Relation   Tinebase_Model_Filter_Relation
264      * * keyfield                String              string   <as defined>                  string            Tinebase_Model_Filter_Text
265      *
266      * * Accepts additional parameter: 'config' => array with these keys:
267      *     - @string appName    (the name of the application of the referenced record/s)
268      *     - @string modelName  (the name of the model of the referenced record/s)
269      *
270      *   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)
271      *     - @string recordClassName 
272      *     - @string controllerClassName
273      *     
274      *   Config for 'records' accepts also these keys:
275      *     - @string refIdField (the field of the foreign record referencing the idProperty of the own record)
276      *     - @array  paging     (accepts the parameters as Tinebase_Model_Pagination does)
277      *     - @string filterClassName
278      *     - @array  addFilters define them as array like Tinebase_Model_Filter_FilterGroup
279      * 
280      * record accepts keys additionally
281      *     - @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
282      * 
283      * <code>
284      *
285      * array(
286      *     'title' => array(
287      *         'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
288      *         'label' => NULL,
289      *         'duplicateCheckGroup' => 'number'
290      *     ),
291      *     'number' => ...
292      * )
293      *
294      * </code>
295      *
296      * @var array
297      */
298     protected $_fields = array();
299
300     /**
301      * holds all field definitions of type records
302      *
303      * @var array
304     */
305     protected $_recordsFields = NULL;
306
307     /**
308      * holds all field definitions of type record (foreignId fields)
309      *
310      * @var array
311      */
312     protected $_recordFields  = NULL;
313
314     /**
315      * holds virtual field definitions used for non-persistent fields getting calculated on each call of the record
316      * no backend property will be build, no filters etc. will exist. they must be filled in frontend json
317      * 
318      * @var array
319      */
320     protected $_virtualFields = NULL;
321     
322     /**
323      * maps fieldgroup keys to their names
324      * Add translation information in comments like: // _('Banking Information')
325      * 
326      * array(
327      *     'banking' => 'Banking Information',    // _('Banking Information')
328      *     'private' => 'Private Information',    // _('Private Information')
329      *     )
330      * 
331      * @var array
332      */
333     
334     protected $_fieldGroups = NULL;
335     /**
336      * here you can define one right (Tinebase_Acl_Rights_Abstract) for each field
337      * group ('group'-property of a field definition of this._fields), the user must
338      * have to see/edit this group, otherwise the fields of the edit dialog will be disabled/readOnly
339      *
340      * array(
341      *     'private' => array(
342      *         'see'  => HumanResources_Acl_Rights::SEE_PRIVATE,
343      *         'edit' => HumanResources_Acl_Rights::EDIT_PRIVATE,
344      *     ),
345      *     'banking' => array(
346      *         'see'  => HumanResources_Acl_Rights::SEE_BANKING,
347      *         'edit' => HumanResources_Acl_Rights::EDIT_BANKING,
348      *     )
349      * );
350      *
351      * @var array
352      */
353     protected $_fieldGroupRights = array();
354
355     /**
356      * every field group will be nested into a fieldset, here you can define the defaults (Ext.Container.defaults)
357      *
358      * @var array
359     */
360     protected $_fieldGroupFeDefaults = array();
361
362     protected $_createModule = FALSE;
363     
364     /*
365      * auto set by the constructor
366     */
367
368     /**
369      * If any field has a group, this will be set to true (autoset by the constructor)
370      *
371      * @var boolean
372     */
373     protected $_useGroups = FALSE;
374
375     /**
376      * the application this configuration belongs to (if the class has the name "Calendar_Model_Event", this will be resolved to "Calendar")
377      *
378      * @var string
379      */
380     protected $_appName = NULL;    // this should be used everytime, everywhere
381     // legacy
382     protected $_application = NULL;
383     protected $_applicationName = NULL;
384     /**
385      * the name of the model (if the class has the name "Calendar_Model_Event", this will be resolved to "Event")
386      *
387      * @var string
388      */
389     protected $_modelName = NULL;
390
391     /**
392      * holds the keys of all fields
393      *
394      * @var array
395      */
396     protected $_fieldKeys = array();
397
398     /**
399      * holds the time fields
400      *
401      * @var array
402     */
403     protected $_timeFields = array();
404
405     /**
406      * holds the fields which will be omitted in the modlog
407      *
408      * @var array
409     */
410     protected $_modlogOmitFields = array();
411
412     /**
413      * these fields will just be readOnly
414      *
415      * @var array
416     */
417     protected $_readOnlyFields = array();
418
419     /**
420      * holds the date and datetime fields
421      *
422      * @var array
423     */
424     protected $_datetimeFields = array();
425
426     /**
427      * holds the alarm datetime fields
428      *
429      * @var array
430     */
431     protected $_alarmDateTimeField = array();
432
433     /**
434      * The calculated filters for this model (auto set)
435      *
436      * @var array
437     */
438     protected $_filterModel = array();
439
440     /**
441      * holds the validators for the model (auto set)
442     */
443     protected $_validators = array();
444
445     /**
446      * holds validators which will be instanciated on model construction
447      *
448      * @var array
449     */
450     protected $_ownValidators = array();
451     
452     /**
453      * if a record is dependent to another, this is true
454      * 
455      * @var boolean
456      */
457     protected $_isDependent = FALSE;
458     
459     /**
460      * input filters (will be set by field configuration)
461      * 
462      * @var array
463      */
464     protected $_filters;
465     
466     /**
467      * Holds the default Data for the model (autoset from field config)
468      *
469      * @var array
470     */
471     protected $_defaultData = array();
472
473     /**
474      * holds the fields / groups to check for duplicates (will be auto set by field configuration)
475     */
476     protected $_duplicateCheckFields = NULL;
477
478     /**
479      * properties to collect for the filters (_appName and _modelName are set in the filter)
480      *
481      * @var array
482      */
483     protected $_filterProperties = array('_filterModel', '_defaultFilter', '_modelName', '_applicationName');
484
485     /**
486      * properties to collect for the model
487      *
488      * @var array
489     */
490     protected $_modelProperties = array(
491         '_identifier', '_timeFields', '_datetimeFields', '_alarmDateTimeField', '_validators', '_modlogOmitFields',
492         '_application', '_readOnlyFields', '_filters'
493     );
494
495     /**
496      * properties to collect for the frontend
497      *
498      * @var array
499     */
500     protected $_frontendProperties = array(
501         'containerProperty', 'containersName', 'containerName', 'defaultSortInfo', 'fieldKeys', 'filterModel',
502         'defaultFilter', 'requiredRight', 'singularContainerMode', 'fields', 'defaultData', 'titleProperty',
503         'useGroups', 'fieldGroupFeDefaults', 'fieldGroupRights', 'multipleEdit', 'multipleEditRequiredRight',
504         'recordName', 'recordsName', 'appName', 'modelName', 'createModule', 'virtualFields', 'group', 'isDependent'
505     );
506
507     /**
508      * the module group (will be on the same leaf of the content type tree panel)
509      * 
510      * @var string
511      */
512     protected $_group = NULL;
513     
514     /**
515      * the backend properties holding the collected properties
516      *
517      * @var array
518     */
519     protected $_modelConfiguration = NULL;
520
521     /**
522      * holds the collected values for the frontendconfig (autoset on first call of getFrontendConfiguration)
523      * 
524      * @var array
525      */
526     protected $_frontendConfiguration = NULL;
527     
528     /**
529      * the backend properties holding the collected properties
530      *
531      * @var array
532      */
533     protected $_filterConfiguration = NULL;
534
535     /*
536      * mappings
537     */
538
539     /**
540      * This defines the filters use for all known types
541      * @var array
542      */
543     protected $_filterModelMapping = array(
544         'date'     => 'Tinebase_Model_Filter_Date',
545         'datetime' => 'Tinebase_Model_Filter_Date',
546         'time'     => 'Tinebase_Model_Filter_Date',
547         'string'   => 'Tinebase_Model_Filter_Text',
548         'text'     => 'Tinebase_Model_Filter_Text',
549         'boolean'  => 'Tinebase_Model_Filter_Bool',
550         'integer'  => 'Tinebase_Model_Filter_Int',
551         'float'    => 'Tinebase_Model_Filter_Int',
552         'record'   => 'Tinebase_Model_Filter_ForeignId',
553         'relation' => 'Tinebase_Model_Filter_Relation',
554
555         'keyfield'  => 'Tinebase_Model_Filter_Text',
556         'container' => 'Tinebase_Model_Filter_Container',
557         'tag'       => 'Tinebase_Model_Filter_Tag',
558         'user'      => 'Tinebase_Model_Filter_User',
559     );
560
561     /**
562      * This maps field types to own validators, which will be instanciated in the constructor.
563      *
564      * @var array
565     */
566     protected $_inputFilterDefaultMapping = array(
567         'text'     => array('Tinebase_Model_InputFilter_CrlfConvert'),
568     );
569
570     /**
571      * This maps field types to their default validators, just zendfw validators can be used here.
572      * For using own validators, use _ownValidatorMapping instead. If no validator is given,
573      * "array(Zend_Filter_Input::ALLOW_EMPTY => true)" will be used
574      *
575      * @var array
576     */
577     protected $_validatorMapping = array(
578         'record'    => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
579         'relation'  => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
580     );
581
582     /**
583      * the constructor (must be called by the singleton pattern)
584      *
585      * @var array $modelClassConfiguration
586      * @throws Tinebase_Exception_Record_DefinitionFailure
587      */
588     public function __construct($modelClassConfiguration)
589     {
590         if (! $modelClassConfiguration) {
591             throw new Tinebase_Exception('The model class configuration must be submitted!');
592         }
593
594         $this->_appName     = $this->_application = $this->_applicationName = $modelClassConfiguration['appName'];
595         
596         // add appName to available applications 
597         self::$_availableApplications[$this->_appName] = TRUE;
598         
599         $this->_modelName   = $modelClassConfiguration['modelName'];
600         $this->_idProperty  = $this->_identifier = array_key_exists('idProperty', $modelClassConfiguration) ? $modelClassConfiguration['idProperty'] : 'id';
601
602         // some cruid validating
603         foreach ($modelClassConfiguration as $propertyName => $propertyValue) {
604             $this->{'_' . $propertyName} = $propertyValue;
605         }
606
607         $this->_fields[$this->_idProperty] = array('label' => NULL, 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true));
608
609         if ($this->_hasCustomFields) {
610             $this->_fields['customfields'] = array('label' => NULL, 'type' => 'custom', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
611         }
612
613         if ($this->_hasRelations) {
614             $this->_fields['relations'] = array('label' => NULL, 'type' => 'relation', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
615         }
616
617         if ($this->_containerProperty) {
618             $this->_fields[$this->_containerProperty] = array(
619                 'label'            => $this->_containerName,
620                 'shy'              => true,
621                 'type'             => 'container',
622                 'validators'       => array(Zend_Filter_Input::ALLOW_EMPTY => true),
623                 'filterDefinition' => array(
624                     'filter'  => $this->_filterModelMapping['container'],
625                     'options' => array('applicationName' => $this->_appName)
626                 )
627             );
628         } else {
629             $this->_singularContainerMode = true;
630         }
631
632         if ($this->_hasTags) {
633             $this->_fields['tags'] = array(
634                 'label' => 'Tags',
635                 'sortable' => false,
636                 'type' => 'tag', 
637                 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL), 
638                 'useGlobalTranslation' => TRUE
639             );
640         }
641
642         if ($this->_modlogActive) {
643             // notes are needed if modlog is active
644             $this->_fields['notes']              = array('label' => NULL,                 'type' => 'note',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL), 'useGlobalTranslation' => TRUE);
645             $this->_fields['created_by']         = array('label' => 'Created By',         'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
646             $this->_fields['creation_time']      = array('label' => 'Creation Time',      'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
647             $this->_fields['last_modified_by']   = array('label' => 'Last Modified By',   'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
648             $this->_fields['last_modified_time'] = array('label' => 'Last Modified Time', 'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'shy' => true, 'useGlobalTranslation' => TRUE);
649
650             // don't show deleted information
651             $this->_fields['deleted_by']         = array('label' => NULL, 'type' => 'user',     'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
652             $this->_fields['deleted_time']       = array('label' => NULL, 'type' => 'datetime', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
653             $this->_fields['is_deleted']         = array('label' => NULL, 'type' => 'boolean',  'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true), 'useGlobalTranslation' => TRUE);
654
655         } elseif ($this->_hasNotes) {
656             $this->_fields['notes'] = array('label' => NULL, 'type' => 'note', 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL));
657         }
658         
659         // holds the filters used for the query-filter, if any
660         $queryFilters = array();
661         
662         foreach ($this->_fields as $fieldKey => &$fieldDef) {
663             // set default type to string, if no type is given
664             if (! array_key_exists('type', $fieldDef)) {
665                 $fieldDef['type'] = 'string';
666             }
667             
668             // don't handle field if app is not available
669             if (array_key_exists('config', $fieldDef) && ($fieldDef['type'] == 'record' || $fieldDef['type'] == 'records') && (! $this->_isAvailable($fieldDef['config']))) {
670                 $fieldDef['type'] = 'string';
671                 $fieldDef['label'] = NULL;
672                 continue;
673             }
674             // the property name
675             $fieldDef['key'] = $fieldKey;
676
677             // if any field has a group set, enable grouping globally
678             if (! $this->_useGroups && array_key_exists('group', $fieldDef)) {
679                 $this->_useGroups = TRUE;
680             }
681
682             if ($fieldDef['type'] == 'virtual') {
683                 $fieldDef['type'] = $fieldDef['config']['type'];
684                 unset($fieldDef['config']['type']);
685                 $this->_virtualFields[] = $fieldDef;
686                 continue;
687             }
688             
689             // set default value
690             // TODO: implement complex default values
691             if (array_key_exists('default', $fieldDef)) {
692 //                 // allows dynamic default values
693 //                 if (is_array($fieldDef['default'])) {
694 //                     switch ($fieldDef['type']) {
695 //                         case 'time':
696 //                         case 'date':
697 //                         case 'datetime':
698 //                         default:
699 //                             throw new Tinebase_Exception_NotImplemented($_message);
700 //                     }
701 //                 } else {
702                     $this->_defaultData[$fieldKey] = $fieldDef['default'];
703                     
704 //                 }
705             }
706
707             // set filter model
708             if (array_key_exists('filterDefinition', $fieldDef)) {
709                 // use filter from definition
710                 $this->_filterModel[$fieldKey] = $fieldDef['filterDefinition'];
711             } else if (array_key_exists($fieldDef['type'], $this->_filterModelMapping)) {
712                 // if no filterDefinition is given, try to use the default one
713                 $this->_filterModel[$fieldKey] = array('filter' => $this->_filterModelMapping[$fieldDef['type']]);
714                 if (array_key_exists('config', $fieldDef)) {
715                     $this->_filterModel[$fieldKey]['options'] = $fieldDef['config'];
716                     
717                     // set id filter controller
718                     if ($fieldDef['type'] == 'record') {
719                         $this->_filterModel[$fieldKey]['options']['filtergroup'] = $fieldDef['config']['appName'] . '_Model_' . $fieldDef['config']['modelName'] . 'Filter';
720                         $this->_filterModel[$fieldKey]['options']['controller']  = $fieldDef['config']['appName'] . '_Controller_' . $fieldDef['config']['modelName'];
721                     }
722                     
723                 }
724             }
725             
726             if (array_key_exists('queryFilter', $fieldDef)) {
727                 $queryFilters[] = $fieldKey;
728             }
729
730             // set validators
731             if (array_key_exists('validators', $fieldDef)) {
732                 // use _validators from definition
733                 $this->_validators[$fieldKey] = $fieldDef['validators'];
734             } else if (array_key_exists($fieldDef['type'], $this->_validatorMapping)) {
735                 // if no validatorsDefinition is given, try to use the default one
736                 $fieldDef['validators'] = $this->_validators[$fieldKey] = $this->_validatorMapping[$fieldDef['type']];
737             } else {
738                 $fieldDef['validators'] = $this->_validators[$fieldKey] = array(Zend_Filter_Input::ALLOW_EMPTY => true);
739             }
740
741             // set input filters, append defined if any or use defaults from _inputFilterDefaultMapping 
742             if (array_key_exists('inputFilters', $fieldDef)) {
743                 foreach ($fieldDef['inputFilters'] as $if => $val) { 
744                     $this->_filters[$fieldKey][] = $if ? new $if($val) : new $val();
745                 }
746             } else if (array_key_exists($fieldDef['type'], $this->_inputFilterDefaultMapping)) {
747                 foreach ($this->_inputFilterDefaultMapping[$fieldDef['type']] as $if => $val) {
748                     $this->_filters[$fieldKey][] = $if ? new $if($val) : new $val();
749                 }
750             }
751             
752             // add field to modlog omit, if configured and modlog is used
753             if ($this->_modlogActive && array_key_exists('modlogOmit', $fieldDef)) {
754                 $this->_modlogOmitFields[] = $fieldKey;
755             }
756
757             // populate properties
758             switch ($fieldDef['type']) {
759                 case 'string':
760                 case 'text':
761                 case 'integer':
762                 case 'float':
763                 case 'boolean':
764                     break;
765                 case 'container':
766                     break;
767                 case 'date':
768                 case 'datetime':
769                     // add to alarm fields
770                     if (array_key_exists('alarm', $fieldDef)) {
771                         $this->_alarmDateTimeField = $fieldKey;
772                     }
773                     // add to datetime fields
774                     $this->_datetimeFields[] = $fieldKey;
775                     break;
776                 case 'time':
777                     // add to timefields
778                     $this->_timeFields[] = $fieldKey;
779                     break;
780                 case 'user':
781                     $fieldDef['config'] = array(
782                         'refIdField'              => 'id',
783                         'appName'                 => 'Addressbook',
784                         'modelName'               => 'Contact',
785                         'recordClassName'         => 'Addressbook_Model_Contact',
786                         'controllerClassName'     => 'Addressbook_Controller_Contact',
787                         'filterClassName'         => 'Addressbook_Model_ContactFilter',
788                         'addFilters' => array(
789                             array('field' => 'type', 'operator' => 'equals', 'value' => 'user')
790                         )
791                     );
792                     $this->_recordFields[$fieldKey] = $fieldDef;
793                     break;
794                 case 'record':
795                     $this->_filterModel[$fieldKey]['options']['controller']  = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Controller');
796                     $this->_filterModel[$fieldKey]['options']['filtergroup'] = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Model') . 'Filter';
797                 case 'records':
798                     $fieldDef['config']['recordClassName']     = array_key_exists('recordClassName', $fieldDef['config'])     ? $fieldDef['config']['recordClassName']     : $this->_getPhpClassName($fieldDef['config']);
799                     $fieldDef['config']['controllerClassName'] = array_key_exists('controllerClassName', $fieldDef['config']) ? $fieldDef['config']['controllerClassName'] : $this->_getPhpClassName($fieldDef['config'], 'Controller');
800                     $fieldDef['config']['filterClassName']     = array_key_exists('filterClassName', $fieldDef['config'])     ? $fieldDef['config']['filterClassName']     : $this->_getPhpClassName($fieldDef['config']) . 'Filter';
801                     if ($fieldDef['type'] == 'record') {
802                         $this->_recordFields[$fieldKey] = $fieldDef;
803                     } else {
804                         $fieldDef['config']['dependentRecords'] = array_key_exists('dependentRecords', $fieldDef['config']) ? $fieldDef['config']['dependentRecords'] : FALSE;
805                         $this->_recordsFields[$fieldKey] = $fieldDef;
806                     }
807                     break;
808                 default:
809                     break;
810             }
811         }
812         
813         // set some default filters
814         if (count($queryFilters)) {
815             $this->_filterModel['query'] = array('label' => 'Quick search', 'field' => 'query', 'filter' => 'Tinebase_Model_Filter_Query', 'options' => array('fields' => $queryFilters), 'useGlobalTranslation' => true);
816         }
817         $this->_filterModel[$this->_idProperty] = array('filter' => 'Tinebase_Model_Filter_Id', 'options' => array('modelName' => $this->_appName . '_Model_' . $this->_modelName));
818         $this->_fieldKeys = array_keys($this->_fields);
819     }
820     
821     /**
822      * gets phpClassName by field definition['config']
823      *
824      * @param array $_fieldConfig
825      * @param string $_type
826      * @return string
827      */
828     protected function _getPhpClassName($_fieldConfig, $_type = 'Model')
829     {
830         return $_fieldConfig['appName'] . '_' . $_type . '_' . $_fieldConfig['modelName'];
831     }
832     
833     /**
834      * checks if app and model is available for the user at record and records fields
835      * later this can be used to use field acl
836      * 
837      * @param array $_fieldConfig the field configuration
838      */
839     protected function _isAvailable($_fieldConfig)
840     {
841         if (! array_key_exists($_fieldConfig['appName'], self::$_availableApplications)) {
842             self::$_availableApplications[$_fieldConfig['appName']] = Tinebase_Application::getInstance()->isInstalled($_fieldConfig['appName'], TRUE);
843         }
844         return self::$_availableApplications[$_fieldConfig['appName']];
845     }
846     
847     /**
848      * returns the filterconfiguration needed in the filtergroup for this model
849      * 
850      * @return array
851      */
852     public function getFilterModel()
853     {
854         // add calculated values to filter configuration
855         if (! $this->_filterConfiguration) {
856             foreach ($this->_filterProperties as $prop) {
857                 $this->_filterConfiguration[$prop] = $this->{$prop};
858             }
859             // @todo: remove this as in the filtergroup
860             $this->_filterConfiguration['_className'] = $this->_appName . '_Model_' . $this->_modelName . 'Filter';
861         }
862         return $this->_filterConfiguration;
863     }
864
865     /**
866      * returns the properties needed for the model
867      * 
868      * @return array
869      */
870     public function toArray()
871     {
872         if (! $this->_modelConfiguration) {
873             // add calculated values to model configuration
874             foreach ($this->_modelProperties as $prop) {
875                 $this->_modelConfiguration[$prop] = $this->{$prop};
876             }
877         }
878         
879         return $this->_modelConfiguration;
880     }
881
882     /**
883      * returns the frontend configuration for creating js models, filters, defaults and some other js stubs.
884      * this will be included in the registry.
885      * Look at Tinebase/js/ApplicationStarter.js
886      */
887     public function getFrontendConfiguration()
888     {
889         if (! $this->_frontendConfiguration) {
890             
891             $this->_frontendConfiguration = array();
892             
893             // add calculated values to frontend configuration
894             foreach ($this->_frontendProperties as $prop) {
895                 $this->_frontendConfiguration[$prop] = $this->{'_' . $prop};
896             }
897         }
898         return $this->_frontendConfiguration;
899     }
900
901     /**
902      * returns default data for this model
903      * 
904      * @return array
905      */
906     public function getDefaultData()
907     {
908         return $this->_defaultData;
909     }
910
911     /**
912      * returns the field configuration of the model
913      */
914     public function getFields()
915     {
916         return $this->_fields;
917     }
918
919     /**
920      * get protected property
921      *
922      * @param string name of property
923      * @throws Tinebase_Exception_UnexpectedValue
924      * @return mixed value of property
925      */
926     public function __get($_property)
927     {
928         if (! property_exists($this,  '_' . $_property)) {
929             throw new Tinebase_Exception_UnexpectedValue('Property does not exist: ' . $_property);
930         }
931         return $this->{'_' . $_property};
932     }
933 }