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