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