0011996: add fallback app icon
[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     /**
968      * get modelconfig for an array of models
969      *
970      * @param array $models
971      * @param string $appname
972      * @return array
973      */
974     public static function getFrontendConfigForModels($models, $appname = null)
975     {
976         $modelconfig = array();
977         foreach ($models as $modelName) {
978             $recordClass = $appname ? $appname . '_Model_' . $modelName : $modelName;
979             $config = $recordClass::getConfiguration();
980             if ($config) {
981                 $modelconfig[$modelName] = $config->getFrontendConfiguration();
982             }
983         }
984
985         return $modelconfig;
986     }
987
988     public function getIdProperty()
989     {
990         return $this->_idProperty;
991     }
992
993     public function getTable()
994     {
995         return $this->_table;
996     }
997
998     public function getVersion()
999     {
1000         return $this->_version;
1001     }
1002
1003     public function getApplName()
1004     {
1005         return $this->_appName;
1006     }
1007
1008     public function getModelName()
1009     {
1010         return $this->_modelName;
1011     }
1012
1013     /**
1014      * populate model config properties
1015      * 
1016      * @param string $fieldKey
1017      * @param array $fieldDef
1018      */
1019     protected function _populateProperties($fieldKey, $fieldDef)
1020     {
1021         switch ($fieldDef['type']) {
1022             case 'string':
1023             case 'text':
1024             case 'integer':
1025             case 'float':
1026             case 'boolean':
1027                 break;
1028             case 'container':
1029                 break;
1030             case 'date':
1031                 // add to datetime fields
1032                 $this->_dateFields[] = $fieldKey;
1033                 break;
1034             case 'datetime':
1035                 // add to alarm fields
1036                 if ((isset($fieldDef['alarm']) || array_key_exists('alarm', $fieldDef))) {
1037                     $this->_alarmDateTimeField = $fieldKey;
1038                 }
1039                 // add to datetime fields
1040                 $this->_datetimeFields[] = $fieldKey;
1041                 break;
1042             case 'time':
1043                 // add to timefields
1044                 $this->_timeFields[] = $fieldKey;
1045                 break;
1046             case 'user':
1047                 $fieldDef['config'] = array(
1048                     'refIdField'              => 'id',
1049                     'length'                  => 40,
1050                     'appName'                 => 'Addressbook',
1051                     'modelName'               => 'Contact',
1052                     'recordClassName'         => 'Addressbook_Model_Contact',
1053                     'controllerClassName'     => 'Addressbook_Controller_Contact',
1054                     'filterClassName'         => 'Addressbook_Model_ContactFilter',
1055                     'addFilters' => array(
1056                         array('field' => 'type', 'operator' => 'equals', 'value' => 'user')
1057                     )
1058                 );
1059                 $this->_recordFields[$fieldKey] = $fieldDef;
1060                 break;
1061             case 'record':
1062                 $this->_filterModel[$fieldKey]['options']['controller']  = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Controller');
1063                 $this->_filterModel[$fieldKey]['options']['filtergroup'] = $this->_getPhpClassName($this->_filterModel[$fieldKey]['options'], 'Model') . 'Filter';
1064             case 'records':
1065                 $fieldDef['config']['recordClassName']     = (isset($fieldDef['config']['recordClassName']) || array_key_exists('recordClassName', $fieldDef['config']))     ? $fieldDef['config']['recordClassName']     : $this->_getPhpClassName($fieldDef['config']);
1066                 $fieldDef['config']['controllerClassName'] = (isset($fieldDef['config']['controllerClassName']) || array_key_exists('controllerClassName', $fieldDef['config'])) ? $fieldDef['config']['controllerClassName'] : $this->_getPhpClassName($fieldDef['config'], 'Controller');
1067                 $fieldDef['config']['filterClassName']     = (isset($fieldDef['config']['filterClassName']) || array_key_exists('filterClassName', $fieldDef['config']))     ? $fieldDef['config']['filterClassName']     : $this->_getPhpClassName($fieldDef['config']) . 'Filter';
1068                 if ($fieldDef['type'] == 'record') {
1069                     $fieldDef['config']['length'] = 40;
1070                     $this->_recordFields[$fieldKey] = $fieldDef;
1071                 } else {
1072                     $fieldDef['config']['dependentRecords'] = (isset($fieldDef['config']['dependentRecords']) || array_key_exists('dependentRecords', $fieldDef['config'])) ? $fieldDef['config']['dependentRecords'] : FALSE;
1073                     $this->_recordsFields[$fieldKey] = $fieldDef;
1074                 }
1075                 break;
1076             case 'custom':
1077                 try {
1078                     // prepend table name to id prop because of ambiguous ids
1079                     // TODO find a better way to get table name, maybe we should put it in the modelconfig?
1080                     $backend = Tinebase_Core::getApplicationInstance($this->_applicationName, $this->_modelName)->getBackend();
1081                     $tableName = $backend->getTableName();
1082                     $this->_filterModel['customfield'] = array(
1083                         'filter' => 'Tinebase_Model_Filter_CustomField', 
1084                         'options' => array(
1085                             'idProperty' => $tableName . '.' . $this->_idProperty
1086                         )
1087                     );
1088                 } catch (Exception $e) {
1089                     // no customfield filter available (yet?)
1090                     Tinebase_Exception::log($e);
1091                 }
1092                 break;
1093             default:
1094                 break;
1095         }
1096     }
1097     
1098     /**
1099      * returns an instance of the record controller
1100      * 
1101      * @return Tinebase_Controller_Record_Interface
1102      */
1103     public function getControllerInstance()
1104     {
1105         return Tinebase_Core::getApplicationInstance($this->_appName, $this->_modelName);
1106     }
1107     
1108     /**
1109      * gets phpClassName by field definition['config']
1110      *
1111      * @param array $_fieldConfig
1112      * @param string $_type
1113      * @return string
1114      */
1115     protected function _getPhpClassName($_fieldConfig = null, $_type = 'Model')
1116     {
1117         if (! $_fieldConfig) {
1118             $_fieldConfig = array('appName' => $this->_appName, 'modelName' => $this->_modelName);
1119         }
1120
1121         return $_fieldConfig['appName'] . '_' . $_type . '_' . $_fieldConfig['modelName'];
1122     }
1123     
1124     /**
1125      * checks if app and model is available for the user at record and records fields
1126      * later this can be used to use field acl
1127      * 
1128      * @param array $_fieldConfig the field configuration
1129      */
1130     protected function _isAvailable($_fieldConfig)
1131     {
1132         if (! (isset(self::$_availableApplications[$_fieldConfig['appName']]) || array_key_exists($_fieldConfig['appName'], self::$_availableApplications))) {
1133             self::$_availableApplications[$_fieldConfig['appName']] = Tinebase_Application::getInstance()->isInstalled($_fieldConfig['appName'], TRUE);
1134         }
1135         return self::$_availableApplications[$_fieldConfig['appName']];
1136     }
1137     
1138     /**
1139      * returns the filterconfiguration needed in the filtergroup for this model
1140      * 
1141      * @return array
1142      */
1143     public function getFilterModel()
1144     {
1145         // add calculated values to filter configuration
1146         if (! $this->_filterConfiguration) {
1147             foreach ($this->_filterProperties as $prop) {
1148                 $this->_filterConfiguration[$prop] = $this->{$prop};
1149             }
1150             // @todo: remove this as in the filtergroup
1151             $this->_filterConfiguration['_className'] = $this->_appName . '_Model_' . $this->_modelName . 'Filter';
1152         }
1153         return $this->_filterConfiguration;
1154     }
1155
1156     /**
1157      * returns the properties needed for the model
1158      * 
1159      * @return array
1160      */
1161     public function toArray()
1162     {
1163         if (! $this->_modelConfiguration) {
1164             // add calculated values to model configuration
1165             foreach ($this->_modelProperties as $prop) {
1166                 $this->_modelConfiguration[$prop] = $this->{$prop};
1167             }
1168         }
1169         
1170         return $this->_modelConfiguration;
1171     }
1172
1173     /**
1174      * returns the frontend configuration for creating js models, filters, defaults and some other js stubs.
1175      * this will be included in the registry.
1176      * Look at Tinebase/js/ApplicationStarter.js
1177      */
1178     public function getFrontendConfiguration()
1179     {
1180         if (! $this->_frontendConfiguration) {
1181             
1182             $this->_frontendConfiguration = array();
1183             
1184             // add calculated values to frontend configuration
1185             foreach ($this->_frontendProperties as $prop) {
1186                 $this->_frontendConfiguration[$prop] = $this->{'_' . $prop};
1187             }
1188         }
1189         return $this->_frontendConfiguration;
1190     }
1191
1192     /**
1193      * returns default data for this model
1194      * 
1195      * @return array
1196      */
1197     public function getDefaultData()
1198     {
1199         return $this->_defaultData;
1200     }
1201
1202     /**
1203      * returns the field configuration of the model
1204      */
1205     public function getFields()
1206     {
1207         return $this->_fields;
1208     }
1209
1210     /**
1211      * returns the converters of the model
1212      */
1213     public function getConverters()
1214     {
1215         return $this->_converters;
1216     }
1217
1218     /**
1219      * get protected property
1220      *
1221      * @param string name of property
1222      * @throws Tinebase_Exception_UnexpectedValue
1223      * @return mixed value of property
1224      */
1225     public function __get($_property)
1226     {
1227         if (! property_exists($this,  '_' . $_property)) {
1228             throw new Tinebase_Exception_UnexpectedValue('Property does not exist: ' . $_property);
1229         }
1230         return $this->{'_' . $_property};
1231     }
1232 }