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