Merge branch '2013.10' into 2014.11
[tine20] / tine20 / Calendar / js / Model.js
1 /* 
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
7  */
8
9 Ext.ns('Tine.Calendar', 'Tine.Calendar.Model');
10
11 /**
12  * @namespace Tine.Calendar.Model
13  * @class Tine.Calendar.Model.Event
14  * @extends Tine.Tinebase.data.Record
15  * Event record definition
16  */
17 Tine.Calendar.Model.Event = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model.genericFields.concat([
18     { name: 'id' },
19     { name: 'dtend', type: 'date', dateFormat: Date.patterns.ISO8601Long },
20     { name: 'transp' },
21     // ical common fields
22     { name: 'class' },
23     { name: 'description' },
24     { name: 'geo' },
25     { name: 'location' },
26     { name: 'organizer' },
27     { name: 'priority' },
28     { name: 'status' },
29     { name: 'summary' },
30     { name: 'url' },
31     { name: 'uid' },
32     // ical common fields with multiple appearance
33     //{ name: 'attach' },
34     { name: 'attendee' },
35     { name: 'alarms'},
36     { name: 'tags' },
37     { name: 'notes'},
38     { name: 'attachments'},
39     //{ name: 'contact' },
40     //{ name: 'related' },
41     //{ name: 'resources' },
42     //{ name: 'rstatus' },
43     // scheduleable interface fields
44     { name: 'dtstart', type: 'date', dateFormat: Date.patterns.ISO8601Long },
45     { name: 'recurid' },
46     { name: 'base_event_id' },
47     // scheduleable interface fields with multiple appearance
48     { name: 'exdate' },
49     //{ name: 'exrule' },
50     //{ name: 'rdate' },
51     { name: 'rrule' },
52     { name: 'mute' },
53     { name: 'is_all_day_event', type: 'bool'},
54     { name: 'rrule_until', type: 'date', dateFormat: Date.patterns.ISO8601Long },
55     { name: 'originator_tz' },
56     // grant helper fields
57     {name: 'readGrant'   , type: 'bool'},
58     {name: 'editGrant'   , type: 'bool'},
59     {name: 'deleteGrant' , type: 'bool'},
60     {name: 'editGrant'   , type: 'bool'},
61     // relations
62     { name: 'relations',   omitDuplicateResolving: true},
63     { name: 'customfields', omitDuplicateResolving: true}
64 ]), {
65     appName: 'Calendar',
66     modelName: 'Event',
67     idProperty: 'id',
68     titleProperty: 'summary',
69     // ngettext('Event', 'Events', n); gettext('Events');
70     recordName: 'Event',
71     recordsName: 'Events',
72     containerProperty: 'container_id',
73     // ngettext('Calendar', 'Calendars', n); gettext('Calendars');
74     containerName: 'Calendar',
75     containersName: 'Calendars',
76     copyOmitFields: ['uid', 'recurid'],
77     
78     /**
79      * mark record out of current filter
80      * 
81      * @type Boolean
82      */
83     outOfFilter: false,
84     
85     /**
86      * returns displaycontainer with orignialcontainer as fallback
87      * 
88      * @return {Array}
89      */
90     getDisplayContainer: function() {
91         var displayContainer = this.get('container_id');
92         var currentAccountId = Tine.Tinebase.registry.get('currentAccount').accountId;
93         
94         var attendeeStore = this.getAttendeeStore();
95         
96         attendeeStore.each(function(attender) {
97             var userAccountId = attender.getUserAccountId();
98             if (userAccountId == currentAccountId) {
99                 var container = attender.get('displaycontainer_id');
100                 if (container) {
101                     displayContainer = container;
102                 }
103                 return false;
104             }
105         }, this);
106         
107         return displayContainer;
108     },
109     
110     /**
111      * is this event a recuring base event?
112      * 
113      * @return {Boolean}
114      */
115     isRecurBase: function() {
116         return !!this.get('rrule') && !this.get('recurid');
117     },
118     
119     /**
120      * is this event a recuring exception?
121      * 
122      * @return {Boolean}
123      */
124     isRecurException: function() {
125         return !! this.get('recurid') && ! this.isRecurInstance();
126     },
127     
128     /**
129      * is this event an recuring event instance?
130      * 
131      * @return {Boolean}
132      */
133     isRecurInstance: function() {
134         return this.id && this.id.match(/^fakeid/);
135     },
136     
137     /**
138      * returns store of attender objects
139      * 
140      * @param  {Array} attendeeData
141      * @return {Ext.data.Store}
142      */
143     getAttendeeStore: function() {
144         return Tine.Calendar.Model.Attender.getAttendeeStore(this.get('attendee'));
145     },
146     
147     /**
148      * returns attender record of current account if exists, else false
149      */
150     getMyAttenderRecord: function() {
151         var attendeeStore = this.getAttendeeStore();
152         return Tine.Calendar.Model.Attender.getAttendeeStore.getMyAttenderRecord(attendeeStore);
153     }
154 });
155
156
157 /**
158  * get default data for a new event
159  *  
160  * @return {Object} default data
161  * @static
162  */ 
163 Tine.Calendar.Model.Event.getDefaultData = function() {
164     var app = Tine.Tinebase.appMgr.get('Calendar'),
165         prefs = app.getRegistry().get('preferences'),
166         defaultAttendeeStrategy = prefs.get('defaultAttendeeStrategy') || 'me',
167         mainScreen = app.getMainScreen(),
168         centerPanel = mainScreen.getCenterPanel(),
169         westPanel = mainScreen.getWestPanel(),
170         container = westPanel.getContainerTreePanel().getDefaultContainer(),
171         organizer = (defaultAttendeeStrategy != 'me' && container.ownerContact) ? container.ownerContact : Tine.Tinebase.registry.get('userContact'),
172         dtstart = new Date().clearTime().add(Date.HOUR, (new Date().getHours() + 1)),
173         period = centerPanel.getCalendarPanel(centerPanel.activeView).getView().getPeriod();
174         
175     // if dtstart is out of current period, take start of current period
176     if (period.from.getTime() > dtstart.getTime() || period.until.getTime() < dtstart.getTime()) {
177         dtstart = period.from.clearTime(true).add(Date.HOUR, 9);
178     }
179     
180     var data = {
181         summary: '',
182         dtstart: dtstart,
183         dtend: dtstart.add(Date.HOUR, 1),
184         container_id: container,
185         transp: 'OPAQUE',
186         editGrant: true,
187         organizer: organizer,
188         attendee: Tine.Calendar.Model.Event.getDefaultAttendee(organizer, container) /*[
189             Ext.apply(Tine.Calendar.Model.Attender.getDefaultData(), {
190                 user_type: 'user',
191                 user_id: Tine.Tinebase.registry.get('userContact'),
192                 status: 'ACCEPTED'
193             })
194         ]*/
195     };
196     
197     if (prefs.get('defaultalarmenabled')) {
198         data.alarms = [{minutes_before: parseInt(prefs.get('defaultalarmminutesbefore'), 10)}];
199     }
200     
201     return data;
202 };
203
204 Tine.Calendar.Model.Event.getDefaultAttendee = function(organizer, container) {
205     var app = Tine.Tinebase.appMgr.get('Calendar'),
206         mainScreen = app.getMainScreen(),
207         centerPanel = mainScreen.getCenterPanel(),
208         westPanel = mainScreen.getWestPanel(),
209         filteredAttendee = westPanel.getAttendeeFilter().getValue() || [],
210         defaultAttendeeData = Tine.Calendar.Model.Attender.getDefaultData(),
211         defaultResourceData = Tine.Calendar.Model.Attender.getDefaultResourceData(),
212         filteredContainers = westPanel.getContainerTreePanel().getFilterPlugin().getFilter().value || [],
213         prefs = app.getRegistry().get('preferences'),
214         defaultAttendeeStrategy = prefs.get('defaultAttendeeStrategy') || 'me', // one of['me', 'intelligent', 'calendarOwner', 'filteredAttendee']
215         defaultAttendee = [];
216         
217     // shift -> change intelligent <-> me
218     if (Ext.EventObject.shiftKey) {
219         defaultAttendeeStrategy = defaultAttendeeStrategy == 'intelligent' ? 'me' :
220                                   defaultAttendeeStrategy == 'me' ? 'intelligent' :
221                                   defaultAttendeeStrategy;
222     }
223     
224     // alt -> prefer calendarOwner in intelligent mode
225     if (defaultAttendeeStrategy == 'intelligent') {
226         defaultAttendeeStrategy = filteredAttendee.length && !Ext.EventObject.altKey > 0 ? 'filteredAttendee' :
227                                   filteredContainers.length > 0 ? 'calendarOwner' :
228                                   'me';
229     }
230     
231     switch(defaultAttendeeStrategy) {
232         case 'me':
233             defaultAttendee.push(Ext.apply(Tine.Calendar.Model.Attender.getDefaultData(), {
234                 user_type: 'user',
235                 user_id: Tine.Tinebase.registry.get('userContact'),
236                 status: 'ACCEPTED'
237             }));
238             break;
239             
240         case 'filteredAttendee':
241             var attendeeStore = Tine.Calendar.Model.Attender.getAttendeeStore(filteredAttendee),
242                 ownAttendee = Tine.Calendar.Model.Attender.getAttendeeStore.getMyAttenderRecord(attendeeStore);
243                 
244             attendeeStore.each(function(attendee){
245                 var attendeeData = attendee.data.user_type == 'user' ? Ext.apply(attendee.data, defaultAttendeeData) : Ext.apply(attendee.data, defaultResourceData);
246                 if (attendee == ownAttendee) {
247                     attendeeData.status = 'ACCEPTED';
248                 }
249                 defaultAttendee.push(attendeeData);
250             }, this);
251             break;
252             
253         case 'calendarOwner':
254             var addedOwnerIds = [];
255             
256             Ext.each(filteredContainers, function(filteredContainer){
257                 if (filteredContainer.ownerContact && filteredContainer.type && filteredContainer.type == 'personal') {
258                     var attendeeData = Ext.apply(Tine.Calendar.Model.Attender.getDefaultData(), {
259                         user_type: 'user',
260                         user_id: filteredContainer.ownerContact
261                     });
262                     
263                     if (attendeeData.user_id.id == organizer.id){
264                         attendeeData.status = 'ACCEPTED';
265                     }
266                     
267                     if (addedOwnerIds.indexOf(filteredContainer.ownerContact.id) < 0) {
268                         defaultAttendee.push(attendeeData);
269                         addedOwnerIds.push(filteredContainer.ownerContact.id);
270                     }
271                 }
272             }, this);
273             
274             if (container.ownerContact && addedOwnerIds.indexOf(container.ownerContact.id) < 0) {
275                 var attendeeData = Ext.apply(Tine.Calendar.Model.Attender.getDefaultData(), {
276                     user_type: 'user',
277                     user_id: container.ownerContact
278                 });
279                 
280                 if (container.ownerContact.id == organizer.id){
281                     attendeeData.status = 'ACCEPTED';
282                 }
283                 
284                 defaultAttendee.push(attendeeData);
285                 addedOwnerIds.push(container.ownerContact.id);
286             }
287             break;
288     }
289     
290     return defaultAttendee;
291 };
292
293 Tine.Calendar.Model.Event.getFilterModel = function() {
294     var app = Tine.Tinebase.appMgr.get('Calendar');
295     
296     return [
297         {label: _('Quick Search'), field: 'query', operators: ['contains']},
298         {label: app.i18n._('Summary'), field: 'summary'},
299         {label: app.i18n._('Location'), field: 'location'},
300         {label: app.i18n._('Description'), field: 'description'},
301         {filtertype: 'tine.widget.container.filtermodel', app: app, recordClass: Tine.Calendar.Model.Event, /*defaultOperator: 'in',*/ defaultValue: {path: Tine.Tinebase.container.getMyNodePath()}},
302         {filtertype: 'calendar.attendee'},
303         {
304             label: app.i18n._('Attendee Status'),
305             field: 'attender_status',
306             filtertype: 'tine.widget.keyfield.filter', 
307             app: app, 
308             keyfieldName: 'attendeeStatus', 
309             defaultOperator: 'notin',
310             defaultValue: ['DECLINED']
311         },
312         {
313             label: app.i18n._('Attendee Role'),
314             field: 'attender_role',
315             filtertype: 'tine.widget.keyfield.filter', 
316             app: app, 
317             keyfieldName: 'attendeeRoles'
318         },
319         {filtertype: 'addressbook.contact', field: 'organizer', label: app.i18n._('Organizer')},
320         {filtertype: 'tinebase.tag', app: app}
321     ];
322 };
323
324 // register calendar filters in addressbook
325 Tine.widgets.grid.ForeignRecordFilter.OperatorRegistry.register('Addressbook', 'Contact', {
326     foreignRecordClass: 'Calendar.Event',
327     linkType: 'foreignId', 
328     filterName: 'ContactAttendeeFilter',
329     // _('Event (as attendee)')
330     label: 'Event (as attendee)'
331 });
332 Tine.widgets.grid.ForeignRecordFilter.OperatorRegistry.register('Addressbook', 'Contact', {
333     foreignRecordClass: 'Calendar.Event',
334     linkType: 'foreignId', 
335     filterName: 'ContactOrganizerFilter',
336     // _('Event (as organizer)')
337     label: 'Event (as organizer)'
338 });
339
340 // example for explicit definition
341 //Tine.widgets.grid.FilterRegistry.register('Addressbook', 'Contact', {
342 //    filtertype: 'foreignrecord',
343 //    foreignRecordClass: 'Calendar.Event',
344 //    linkType: 'foreignId', 
345 //    filterName: 'ContactAttendeeFilter',
346 //    // _('Event attendee')
347 //    label: 'Event attendee'
348 //});
349
350 /**
351  * @namespace Tine.Calendar.Model
352  * @class Tine.Calendar.Model.EventJsonBackend
353  * @extends Tine.Tinebase.data.RecordProxy
354  * 
355  * JSON backend for events
356  */
357 Tine.Calendar.Model.EventJsonBackend = Ext.extend(Tine.Tinebase.data.RecordProxy, {
358     
359     /**
360      * Creates a recuring event exception
361      * 
362      * @param {Tine.Calendar.Model.Event} event
363      * @param {Boolean} deleteInstance
364      * @param {Boolean} deleteAllFollowing
365      * @param {Object} options
366      * @return {String} transaction id
367      */
368     createRecurException: function(event, deleteInstance, deleteAllFollowing, checkBusyConflicts, options) {
369         options = options || {};
370         options.params = options.params || {};
371         options.beforeSuccess = function(response) {
372             return [this.recordReader(response)];
373         };
374         
375         var p = options.params;
376         p.method = this.appName + '.createRecurException';
377         p.recordData = event.data;
378         p.deleteInstance = deleteInstance ? 1 : 0;
379         p.deleteAllFollowing = deleteAllFollowing ? 1 : 0;
380         p.checkBusyConflicts = checkBusyConflicts ? 1 : 0;
381         
382         return this.doXHTTPRequest(options);
383     },
384     
385     /**
386      * delete a recuring event series
387      * 
388      * @param {Tine.Calendar.Model.Event} event
389      * @param {Object} options
390      * @return {String} transaction id
391      */
392     deleteRecurSeries: function(event, options) {
393         options = options || {};
394         options.params = options.params || {};
395         
396         var p = options.params;
397         p.method = this.appName + '.deleteRecurSeries';
398         p.recordData = event.data;
399         
400         return this.doXHTTPRequest(options);
401     },
402     
403     
404     /**
405      * updates a recuring event series
406      * 
407      * @param {Tine.Calendar.Model.Event} event
408      * @param {Object} options
409      * @return {String} transaction id
410      */
411     updateRecurSeries: function(event, checkBusyConflicts, options) {
412         options = options || {};
413         options.params = options.params || {};
414         options.beforeSuccess = function(response) {
415             return [this.recordReader(response)];
416         };
417         
418         var p = options.params;
419         p.method = this.appName + '.updateRecurSeries';
420         p.recordData = event.data;
421         p.checkBusyConflicts = checkBusyConflicts ? 1 : 0;
422         
423         return this.doXHTTPRequest(options);
424     }
425 });
426
427 /*
428  * default event backend
429  */
430 if (Tine.Tinebase.widgets) {
431     Tine.Calendar.backend = new Tine.Calendar.Model.EventJsonBackend({
432         appName: 'Calendar',
433         modelName: 'Event',
434         recordClass: Tine.Calendar.Model.Event
435     });
436 } else {
437     Tine.Calendar.backend = new Tine.Tinebase.data.MemoryBackend({
438         appName: 'Calendar',
439         modelName: 'Event',
440         recordClass: Tine.Calendar.Model.Event
441     });
442 }
443
444 /**
445  * @namespace Tine.Calendar.Model
446  * @class Tine.Calendar.Model.Attender
447  * @extends Tine.Tinebase.data.Record
448  * Attender Record Definition
449  */
450 Tine.Calendar.Model.Attender = Tine.Tinebase.data.Record.create([
451     {name: 'id'},
452     {name: 'cal_event_id'},
453     {name: 'user_id', sortType: Tine.Tinebase.common.accountSortType },
454     {name: 'user_type'},
455     {name: 'role', type: 'keyField', keyFieldConfigName: 'attendeeRoles'},
456     {name: 'quantity'},
457     {name: 'status', type: 'keyField', keyFieldConfigName: 'attendeeStatus'},
458     {name: 'status_authkey'},
459     {name: 'displaycontainer_id'},
460     {name: 'transp'},
461     {name: 'checked'} // filter grid helper field
462 ], {
463     appName: 'Calendar',
464     modelName: 'Attender',
465     idProperty: 'id',
466     titleProperty: 'name',
467     // ngettext('Attender', 'Attendee', n); gettext('Attendee');
468     recordName: 'Attender',
469     recordsName: 'Attendee',
470     containerProperty: 'cal_event_id',
471     // ngettext('Event', 'Events', n); gettext('Events');
472     containerName: 'Event',
473     containersName: 'Events',
474     
475     /**
476      * gets name of attender
477      * 
478      * @return {String}
479      *
480     getName: function() {
481         var user_id = this.get('user_id');
482         if (! user_id) {
483             return Tine.Tinebase.appMgr.get('Calendar').i18n._('No Information');
484         }
485         
486         var userData = (typeof user_id.get == 'function') ? user_id.data : user_id;
487     },
488     */
489     
490     /**
491      * returns account_id if attender is/has a user account
492      * 
493      * @return {String}
494      */
495     getUserAccountId: function() {
496         var user_type = this.get('user_type');
497         if (user_type == 'user' || user_type == 'groupmember') {
498             var user_id = this.get('user_id');
499             if (! user_id) {
500                 return null;
501             }
502             
503             // we expect user_id to be a user or contact object or record
504             if (typeof user_id.get == 'function') {
505                 if (user_id.get('contact_id')) {
506                     // user_id is a account record
507                     return user_id.get('accountId');
508                 } else {
509                     // user_id is a contact record
510                     return user_id.get('account_id');
511                 }
512             } else if (user_id.hasOwnProperty('contact_id')) {
513                 // user_id contains account data
514                 return user_id.accountId;
515             } else if (user_id.hasOwnProperty('account_id')) {
516                 // user_id contains contact data
517                 return user_id.account_id;
518             }
519             
520             // this might happen if contact resolved, due to right restrictions
521             return user_id;
522             
523         }
524         return null;
525     },
526     
527     /**
528      * returns id of attender of any kind
529      */
530     getUserId: function() {
531         var user_id = this.get('user_id');
532         if (! user_id) {
533             return null;
534         }
535         
536         var userData = (typeof user_id.get == 'function') ? user_id.data : user_id;
537         
538         if (!userData) {
539             return null;
540         }
541         
542         if (typeof userData != 'object') {
543             return userData;
544         }
545         
546         switch (this.get('user_type')) {
547             case 'user':
548             case 'groupmember':
549             case 'memberOf':
550                 if (userData.hasOwnProperty('contact_id')) {
551                     // userData contains account
552                     return userData.contact_id;
553                 } else if (userData.hasOwnProperty('account_id')) {
554                     // userData contains contact
555                     return userData.id;
556                 } else if (userData.group_id) {
557                     // userData contains list
558                     return userData.id;
559                 } else if (userData.list_id) {
560                     // userData contains group
561                     return userData.list_id;
562                 }
563                 break;
564             default:
565                 return userData.id
566                 break;
567         }
568     }
569 });
570
571 /**
572  * @namespace Tine.Calendar.Model
573  * 
574  * get default data for a new attender
575  *  
576  * @return {Object} default data
577  * @static
578  */ 
579 Tine.Calendar.Model.Attender.getDefaultData = function() {
580     return {
581         user_type: 'user',
582         role: 'REQ',
583         quantity: 1,
584         status: 'NEEDS-ACTION'
585     };
586 };
587
588 /**
589  * @namespace Tine.Calendar.Model
590  * 
591  * get default data for a new resource
592  *  
593  * @return {Object} default data
594  * @static
595  */ 
596 Tine.Calendar.Model.Attender.getDefaultResourceData = function() {
597     return {
598         user_type: 'resource',
599         role: 'REQ',
600         quantity: 1,
601         status: 'NEEDS-ACTION'
602     };
603 };
604
605 /**
606  * @namespace Tine.Calendar.Model
607  * 
608  * creates store of attender objects
609  * 
610  * @param  {Array} attendeeData
611  * @return {Ext.data.Store}
612  * @static
613  */ 
614 Tine.Calendar.Model.Attender.getAttendeeStore = function(attendeeData) {
615     var attendeeStore = new Ext.data.SimpleStore({
616         fields: Tine.Calendar.Model.Attender.getFieldDefinitions(),
617         sortInfo: {field: 'user_id', direction: 'ASC'}
618     });
619     
620     Ext.each(attendeeData, function(attender) {
621         if (attender) {
622             var record = new Tine.Calendar.Model.Attender(attender, attender.id && Ext.isString(attender.id) ? attender.id : Ext.id());
623             attendeeStore.addSorted(record);
624         }
625     });
626     
627     return attendeeStore;
628 };
629
630 /**
631  * returns attender record of current account if exists, else false
632  * @static
633  */
634 Tine.Calendar.Model.Attender.getAttendeeStore.getMyAttenderRecord = function(attendeeStore) {
635         var currentAccountId = Tine.Tinebase.registry.get('currentAccount').accountId;
636         var myRecord = false;
637         
638         attendeeStore.each(function(attender) {
639             var userAccountId = attender.getUserAccountId();
640             if (userAccountId == currentAccountId) {
641                 myRecord = attender;
642                 return false;
643             }
644         }, this);
645         
646         return myRecord;
647     }
648     
649 /**
650  * returns attendee record of given attendee if exists, else false
651  * @static
652  */
653 Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord = function(attendeeStore, attendee) {
654     var attendeeRecord = false;
655     
656     attendeeStore.each(function(r) {
657         if (r.get('user_type') == attendee.get('user_type') && r.getUserId() == attendee.getUserId()) {
658             attendeeRecord = r;
659             return false;
660         }
661     }, this);
662     
663     return attendeeRecord;
664 }
665
666 /**
667  * @namespace Tine.Calendar.Model
668  * @class Tine.Calendar.Model.Resource
669  * @extends Tine.Tinebase.data.Record
670  * Resource Record Definition
671  */
672 Tine.Calendar.Model.Resource = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model.genericFields.concat([
673     {name: 'id'},
674     {name: 'name'},
675     {name: 'description'},
676     {name: 'email'},
677     {name: 'is_location', type: 'bool'},
678     {name: 'tags'},
679     {name: 'notes'},
680     {name: 'grants'}
681 ]), {
682     appName: 'Calendar',
683     modelName: 'Resource',
684     idProperty: 'id',
685     titleProperty: 'name',
686     // ngettext('Resource', 'Resources', n); gettext('Resources');
687     recordName: 'Resource',
688     recordsName: 'Resources'
689 });
690
691 /**
692  * @namespace   Tine.Calendar.Model
693  * @class       Tine.Calendar.Model.iMIP
694  * @extends     Tine.Tinebase.data.Record
695  * iMIP Record Definition
696  */
697 Tine.Calendar.Model.iMIP = Tine.Tinebase.data.Record.create([
698     {name: 'id'},
699     {name: 'ics'},
700     {name: 'method'},
701     {name: 'originator'},
702     {name: 'userAgent'},
703     {name: 'event'},
704     {name: 'existing_event'},
705     {name: 'preconditions'}
706 ], {
707     appName: 'Calendar',
708     modelName: 'iMIP',
709     idProperty: 'id'
710 });