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