12398 groupmember filter adds wrong attendee
[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, container)
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, container) {
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 = Ext.applyIf(Ext.decode(Ext.encode(attendee.data)), defaultAttendeeData);
253
254                 switch (attendeeData.user_type.toLowerCase()) {
255                     case 'memberof':
256                         attendeeData.user_type = 'group';
257                         break;
258                     case 'resource':
259                         Ext.apply(attendeeData, defaultResourceData);
260                         break;
261                     default:
262                         break;
263                 }
264
265                 if (attendee == ownAttendee) {
266                     attendeeData.status = 'ACCEPTED';
267                 }
268                 defaultAttendee.push(attendeeData);
269             }, this);
270             break;
271             
272         case 'calendarOwner':
273             var addedOwnerIds = [];
274             
275             Ext.each(filteredContainers, function(filteredContainer){
276                 if (filteredContainer.ownerContact && filteredContainer.type && filteredContainer.type == 'personal') {
277                     var attendeeData = Ext.apply(Tine.Calendar.Model.Attender.getDefaultData(), {
278                         user_type: 'user',
279                         user_id: filteredContainer.ownerContact
280                     });
281                     
282                     if (attendeeData.user_id.id == organizer.id){
283                         attendeeData.status = 'ACCEPTED';
284                     }
285                     
286                     if (addedOwnerIds.indexOf(filteredContainer.ownerContact.id) < 0) {
287                         defaultAttendee.push(attendeeData);
288                         addedOwnerIds.push(filteredContainer.ownerContact.id);
289                     }
290                 }
291             }, this);
292             
293             if (container.ownerContact && addedOwnerIds.indexOf(container.ownerContact.id) < 0) {
294                 var attendeeData = Ext.apply(Tine.Calendar.Model.Attender.getDefaultData(), {
295                     user_type: 'user',
296                     user_id: container.ownerContact
297                 });
298                 
299                 if (container.ownerContact.id == organizer.id){
300                     attendeeData.status = 'ACCEPTED';
301                 }
302                 
303                 defaultAttendee.push(attendeeData);
304                 addedOwnerIds.push(container.ownerContact.id);
305             }
306             break;
307     }
308     
309     return defaultAttendee;
310 };
311
312 Tine.Calendar.Model.Event.getFilterModel = function() {
313     var app = Tine.Tinebase.appMgr.get('Calendar');
314     
315     return [
316         {label: _('Quick Search'), field: 'query', operators: ['contains']},
317         {label: app.i18n._('Summary'), field: 'summary'},
318         {label: app.i18n._('Location'), field: 'location'},
319         {label: app.i18n._('Description'), field: 'description'},
320         {filtertype: 'tine.widget.container.filtermodel', app: app, recordClass: Tine.Calendar.Model.Event, /*defaultOperator: 'in',*/ defaultValue: {path: Tine.Tinebase.container.getMyNodePath()}},
321         {filtertype: 'calendar.attendee'},
322         {
323             label: app.i18n._('Attendee Status'),
324             field: 'attender_status',
325             filtertype: 'tine.widget.keyfield.filter', 
326             app: app, 
327             keyfieldName: 'attendeeStatus', 
328             defaultOperator: 'notin',
329             defaultValue: ['DECLINED']
330         },
331         {
332             label: app.i18n._('Attendee Role'),
333             field: 'attender_role',
334             filtertype: 'tine.widget.keyfield.filter', 
335             app: app, 
336             keyfieldName: 'attendeeRoles'
337         },
338         {filtertype: 'addressbook.contact', field: 'organizer', label: app.i18n._('Organizer')},
339         {filtertype: 'tinebase.tag', app: app}
340     ];
341 };
342
343 // register grants for calendar containers
344 Tine.widgets.container.GrantsManager.register('Calendar_Model_Event', function(container) {
345     var grants = Tine.widgets.container.GrantsManager.defaultGrants();
346
347     if (container.type == 'personal') {
348         grants.push('freebusy');
349     }
350     if (container.type == 'personal' && container.capabilites_private) {
351         grants.push('private');
352     }
353
354     return grants;
355 });
356
357 // register calendar filters in addressbook
358 Tine.widgets.grid.ForeignRecordFilter.OperatorRegistry.register('Addressbook', 'Contact', {
359     foreignRecordClass: 'Calendar.Event',
360     linkType: 'foreignId', 
361     filterName: 'ContactAttendeeFilter',
362     // _('Event (as attendee)')
363     label: 'Event (as attendee)'
364 });
365 Tine.widgets.grid.ForeignRecordFilter.OperatorRegistry.register('Addressbook', 'Contact', {
366     foreignRecordClass: 'Calendar.Event',
367     linkType: 'foreignId', 
368     filterName: 'ContactOrganizerFilter',
369     // _('Event (as organizer)')
370     label: 'Event (as organizer)'
371 });
372
373 // example for explicit definition
374 //Tine.widgets.grid.FilterRegistry.register('Addressbook', 'Contact', {
375 //    filtertype: 'foreignrecord',
376 //    foreignRecordClass: 'Calendar.Event',
377 //    linkType: 'foreignId', 
378 //    filterName: 'ContactAttendeeFilter',
379 //    // _('Event attendee')
380 //    label: 'Event attendee'
381 //});
382
383 /**
384  * @namespace Tine.Calendar.Model
385  * @class Tine.Calendar.Model.EventJsonBackend
386  * @extends Tine.Tinebase.data.RecordProxy
387  * 
388  * JSON backend for events
389  */
390 Tine.Calendar.Model.EventJsonBackend = Ext.extend(Tine.Tinebase.data.RecordProxy, {
391     
392     /**
393      * Creates a recuring event exception
394      * 
395      * @param {Tine.Calendar.Model.Event} event
396      * @param {Boolean} deleteInstance
397      * @param {Boolean} deleteAllFollowing
398      * @param {Object} options
399      * @return {String} transaction id
400      */
401     createRecurException: function(event, deleteInstance, deleteAllFollowing, checkBusyConflicts, options) {
402         options = options || {};
403         options.params = options.params || {};
404         options.beforeSuccess = function(response) {
405             return [this.recordReader(response)];
406         };
407         
408         var p = options.params;
409         p.method = this.appName + '.createRecurException';
410         p.recordData = event.data;
411         p.deleteInstance = deleteInstance ? 1 : 0;
412         p.deleteAllFollowing = deleteAllFollowing ? 1 : 0;
413         p.checkBusyConflicts = checkBusyConflicts ? 1 : 0;
414         
415         return this.doXHTTPRequest(options);
416     },
417     
418     /**
419      * delete a recuring event series
420      * 
421      * @param {Tine.Calendar.Model.Event} event
422      * @param {Object} options
423      * @return {String} transaction id
424      */
425     deleteRecurSeries: function(event, options) {
426         options = options || {};
427         options.params = options.params || {};
428         
429         var p = options.params;
430         p.method = this.appName + '.deleteRecurSeries';
431         p.recordData = event.data;
432         
433         return this.doXHTTPRequest(options);
434     },
435     
436     
437     /**
438      * updates a recuring event series
439      * 
440      * @param {Tine.Calendar.Model.Event} event
441      * @param {Object} options
442      * @return {String} transaction id
443      */
444     updateRecurSeries: function(event, checkBusyConflicts, options) {
445         options = options || {};
446         options.params = options.params || {};
447         options.beforeSuccess = function(response) {
448             return [this.recordReader(response)];
449         };
450         
451         var p = options.params;
452         p.method = this.appName + '.updateRecurSeries';
453         p.recordData = event.data;
454         p.checkBusyConflicts = checkBusyConflicts ? 1 : 0;
455         
456         return this.doXHTTPRequest(options);
457     }
458 });
459
460 /*
461  * default event backend
462  */
463 if (Tine.Tinebase.widgets) {
464     Tine.Calendar.backend = new Tine.Calendar.Model.EventJsonBackend({
465         appName: 'Calendar',
466         modelName: 'Event',
467         recordClass: Tine.Calendar.Model.Event
468     });
469 } else {
470     Tine.Calendar.backend = new Tine.Tinebase.data.MemoryBackend({
471         appName: 'Calendar',
472         modelName: 'Event',
473         recordClass: Tine.Calendar.Model.Event
474     });
475 }
476
477 /**
478  * @namespace Tine.Calendar.Model
479  * @class Tine.Calendar.Model.Attender
480  * @extends Tine.Tinebase.data.Record
481  * Attender Record Definition
482  */
483 Tine.Calendar.Model.Attender = Tine.Tinebase.data.Record.create([
484     {name: 'id'},
485     {name: 'cal_event_id'},
486     {name: 'user_id', sortType: Tine.Tinebase.common.accountSortType },
487     {name: 'user_type'},
488     {name: 'role', type: 'keyField', keyFieldConfigName: 'attendeeRoles'},
489     {name: 'quantity'},
490     {name: 'status', type: 'keyField', keyFieldConfigName: 'attendeeStatus'},
491     {name: 'status_authkey'},
492     {name: 'displaycontainer_id'},
493     {name: 'transp'},
494     {name: 'checked'} // filter grid helper field
495 ], {
496     appName: 'Calendar',
497     modelName: 'Attender',
498     idProperty: 'id',
499     titleProperty: 'name',
500     // ngettext('Attender', 'Attendee', n); gettext('Attendee');
501     recordName: 'Attender',
502     recordsName: 'Attendee',
503     containerProperty: 'cal_event_id',
504     // ngettext('Event', 'Events', n); gettext('Events');
505     containerName: 'Event',
506     containersName: 'Events',
507     
508     /**
509      * gets name of attender
510      * 
511      * @return {String}
512      *
513     getName: function() {
514         var user_id = this.get('user_id');
515         if (! user_id) {
516             return Tine.Tinebase.appMgr.get('Calendar').i18n._('No Information');
517         }
518         
519         var userData = (typeof user_id.get == 'function') ? user_id.data : user_id;
520     },
521     */
522     
523     /**
524      * returns account_id if attender is/has a user account
525      * 
526      * @return {String}
527      */
528     getUserAccountId: function() {
529         var user_type = this.get('user_type');
530         if (user_type == 'user' || user_type == 'groupmember') {
531             var user_id = this.get('user_id');
532             if (! user_id) {
533                 return null;
534             }
535             
536             // we expect user_id to be a user or contact object or record
537             if (typeof user_id.get == 'function') {
538                 if (user_id.get('contact_id')) {
539                     // user_id is a account record
540                     return user_id.get('accountId');
541                 } else {
542                     // user_id is a contact record
543                     return user_id.get('account_id');
544                 }
545             } else if (user_id.hasOwnProperty('contact_id')) {
546                 // user_id contains account data
547                 return user_id.accountId;
548             } else if (user_id.hasOwnProperty('account_id')) {
549                 // user_id contains contact data
550                 return user_id.account_id;
551             }
552             
553             // this might happen if contact resolved, due to right restrictions
554             return user_id;
555             
556         }
557         return null;
558     },
559     
560     /**
561      * returns id of attender of any kind
562      */
563     getUserId: function() {
564         var user_id = this.get('user_id');
565         if (! user_id) {
566             return null;
567         }
568         
569         var userData = (typeof user_id.get == 'function') ? user_id.data : user_id;
570         
571         if (!userData) {
572             return null;
573         }
574         
575         if (typeof userData != 'object') {
576             return userData;
577         }
578         
579         switch (this.get('user_type')) {
580             case 'user':
581             case 'groupmember':
582             case 'memberOf':
583                 if (userData.hasOwnProperty('contact_id')) {
584                     // userData contains account
585                     return userData.contact_id;
586                 } else if (userData.hasOwnProperty('account_id')) {
587                     // userData contains contact
588                     return userData.id;
589                 } else if (userData.group_id) {
590                     // userData contains list
591                     return userData.id;
592                 } else if (userData.list_id) {
593                     // userData contains group
594                     return userData.list_id;
595                 }
596                 break;
597             default:
598                 return userData.id
599                 break;
600         }
601     }
602 });
603
604 /**
605  * @namespace Tine.Calendar.Model
606  * 
607  * get default data for a new attender
608  *  
609  * @return {Object} default data
610  * @static
611  */ 
612 Tine.Calendar.Model.Attender.getDefaultData = function() {
613     return {
614         user_type: 'user',
615         role: 'REQ',
616         quantity: 1,
617         status: 'NEEDS-ACTION'
618     };
619 };
620
621 /**
622  * @namespace Tine.Calendar.Model
623  * 
624  * get default data for a new resource
625  *  
626  * @return {Object} default data
627  * @static
628  */ 
629 Tine.Calendar.Model.Attender.getDefaultResourceData = function() {
630     return {
631         user_type: 'resource',
632         role: 'REQ',
633         quantity: 1,
634         status: 'NEEDS-ACTION'
635     };
636 };
637
638 /**
639  * @namespace Tine.Calendar.Model
640  * 
641  * creates store of attender objects
642  * 
643  * @param  {Array} attendeeData
644  * @return {Ext.data.Store}
645  * @static
646  */ 
647 Tine.Calendar.Model.Attender.getAttendeeStore = function(attendeeData) {
648     var attendeeStore = new Ext.data.SimpleStore({
649         fields: Tine.Calendar.Model.Attender.getFieldDefinitions(),
650         sortInfo: {field: 'user_id', direction: 'ASC'}
651     });
652     
653     Ext.each(attendeeData, function(attender) {
654         if (attender) {
655             var record = new Tine.Calendar.Model.Attender(attender, attender.id && Ext.isString(attender.id) ? attender.id : Ext.id());
656             attendeeStore.addSorted(record);
657         }
658     });
659     
660     return attendeeStore;
661 };
662
663 /**
664  * returns attender record of current account if exists, else false
665  * @static
666  */
667 Tine.Calendar.Model.Attender.getAttendeeStore.getMyAttenderRecord = function(attendeeStore) {
668     var currentAccountId = Tine.Tinebase.registry.get('currentAccount').accountId;
669     var myRecord = false;
670
671     attendeeStore.each(function(attender) {
672         var userAccountId = attender.getUserAccountId();
673         if (userAccountId == currentAccountId) {
674             myRecord = attender;
675             return false;
676         }
677     }, this);
678
679     return myRecord;
680 };
681     
682 /**
683  * returns attendee record of given attendee if exists, else false
684  * @static
685  */
686 Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord = function(attendeeStore, attendee) {
687     var attendeeRecord = false;
688     
689     attendeeStore.each(function(r) {
690         var attendeeType = [attendee.get('user_type')];
691
692         // add groupmember for user
693         if (attendeeType[0] == 'user') {
694             attendeeType.push('groupmember');
695         }
696
697         if (attendeeType.indexOf(r.get('user_type')) >= 0 && r.getUserId() == attendee.getUserId()) {
698             attendeeRecord = r;
699             return false;
700         }
701     }, this);
702     
703     return attendeeRecord;
704 };
705
706 /**
707  * returns attendee data
708  * optinally fills into event record
709  */
710 Tine.Calendar.Model.Attender.getAttendeeStore.getData = function(attendeeStore, event) {
711     var attendeeData = [];
712
713     Tine.Tinebase.common.assertComparable(attendeeData);
714
715     attendeeStore.each(function (attender) {
716         var user_id = attender.get('user_id');
717         if (user_id/* && user_id.id*/) {
718             if (typeof user_id.get == 'function') {
719                 attender.data.user_id = user_id.data;
720             }
721
722             attendeeData.push(attender.data);
723         }
724     }, this);
725
726     if (event) {
727         event.set('attendee', attendeeData);
728     }
729
730     return attendeeData;
731 }
732
733 /**
734  * @namespace Tine.Calendar.Model
735  * @class Tine.Calendar.Model.Resource
736  * @extends Tine.Tinebase.data.Record
737  * Resource Record Definition
738  */
739 Tine.Calendar.Model.Resource = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model.genericFields.concat([
740     {name: 'id'},
741     {name: 'name'},
742     {name: 'description'},
743     {name: 'email'},
744     {name: 'is_location', type: 'bool'},
745     {name: 'status', type: 'keyField', keyFieldConfigName: 'attendeeStatus'},
746     {name: 'suppress_notification', type: 'bool'},
747     {name: 'tags'},
748     {name: 'notes'},
749     {name: 'grants'}
750 ]), {
751     appName: 'Calendar',
752     modelName: 'Resource',
753     idProperty: 'id',
754     titleProperty: 'name',
755     // ngettext('Resource', 'Resources', n); gettext('Resources');
756     recordName: 'Resource',
757     recordsName: 'Resources'
758 });
759
760 /**
761  * @namespace   Tine.Calendar.Model
762  * @class       Tine.Calendar.Model.iMIP
763  * @extends     Tine.Tinebase.data.Record
764  * iMIP Record Definition
765  */
766 Tine.Calendar.Model.iMIP = Tine.Tinebase.data.Record.create([
767     {name: 'id'},
768     {name: 'ics'},
769     {name: 'method'},
770     {name: 'originator'},
771     {name: 'userAgent'},
772     {name: 'event'},
773     {name: 'existing_event'},
774     {name: 'preconditions'}
775 ], {
776     appName: 'Calendar',
777     modelName: 'iMIP',
778     idProperty: 'id'
779 });