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