empty string can't be decoded
[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     promiseCreateRecurException: function(event, deleteInstance, deleteAllFollowing, checkBusyConflicts, options) {
451         var me = this;
452         return new Promise(function (fulfill, reject) {
453             try {
454                 me.createRecurException(event, deleteInstance, deleteAllFollowing, checkBusyConflicts, Ext.apply(options || {}, {
455                     success: function (r) {
456                         fulfill(r);
457                     },
458                     failure: function (error) {
459                         reject(new Error(error));
460                     }
461                 }));
462             } catch (error) {
463                 if (Ext.isFunction(reject)) {
464                     reject(new Error(options));
465                 }
466             }
467         });
468     },
469
470     /**
471      * delete a recuring event series
472      * 
473      * @param {Tine.Calendar.Model.Event} event
474      * @param {Object} options
475      * @return {String} transaction id
476      */
477     deleteRecurSeries: function(event, options) {
478         options = options || {};
479         options.params = options.params || {};
480         
481         var p = options.params;
482         p.method = this.appName + '.deleteRecurSeries';
483         p.recordData = event.data;
484         
485         return this.doXHTTPRequest(options);
486     },
487     
488     
489     /**
490      * updates a recuring event series
491      * 
492      * @param {Tine.Calendar.Model.Event} event
493      * @param {Object} options
494      * @return {String} transaction id
495      */
496     updateRecurSeries: function(event, checkBusyConflicts, options) {
497         options = options || {};
498         options.params = options.params || {};
499         options.beforeSuccess = function(response) {
500             return [this.recordReader(response)];
501         };
502         
503         var p = options.params;
504         p.method = this.appName + '.updateRecurSeries';
505         p.recordData = event.data;
506         p.checkBusyConflicts = checkBusyConflicts ? 1 : 0;
507         
508         return this.doXHTTPRequest(options);
509     }
510 });
511
512 /*
513  * default event backend
514  */
515 if (Tine.Tinebase.widgets) {
516     Tine.Calendar.backend = new Tine.Calendar.Model.EventJsonBackend({
517         appName: 'Calendar',
518         modelName: 'Event',
519         recordClass: Tine.Calendar.Model.Event
520     });
521 } else {
522     Tine.Calendar.backend = new Tine.Tinebase.data.MemoryBackend({
523         appName: 'Calendar',
524         modelName: 'Event',
525         recordClass: Tine.Calendar.Model.Event
526     });
527 }
528
529 /**
530  * @namespace Tine.Calendar.Model
531  * @class Tine.Calendar.Model.Attender
532  * @extends Tine.Tinebase.data.Record
533  * Attender Record Definition
534  */
535 Tine.Calendar.Model.Attender = Tine.Tinebase.data.Record.create([
536     {name: 'id'},
537     {name: 'cal_event_id'},
538     {name: 'user_id', sortType: Tine.Tinebase.common.accountSortType },
539     {name: 'user_type'},
540     {name: 'role', type: 'keyField', keyFieldConfigName: 'attendeeRoles'},
541     {name: 'quantity'},
542     {name: 'status', type: 'keyField', keyFieldConfigName: 'attendeeStatus'},
543     {name: 'status_authkey'},
544     {name: 'displaycontainer_id'},
545     {name: 'transp'},
546     {name: 'checked'}, // filter grid helper field
547     {name: 'fbInfo'}   // helper field
548 ], {
549     appName: 'Calendar',
550     modelName: 'Attender',
551     idProperty: 'id',
552     titleProperty: 'name',
553     // ngettext('Attender', 'Attendee', n); gettext('Attendee');
554     recordName: 'Attender',
555     recordsName: 'Attendee',
556     containerProperty: 'cal_event_id',
557     // ngettext('Event', 'Events', n); gettext('Events');
558     containerName: 'Event',
559     containersName: 'Events',
560     
561     /**
562      * gets name of attender
563      * 
564      * @return {String}
565      */
566     getTitle: function() {
567         var p = Tine.Calendar.AttendeeGridPanel.prototype;
568         return p.renderAttenderName.call(p, this.get('user_id'), false, this);
569     },
570
571     getCompoundId: function(mapGroupmember) {
572         var type = this.get('user_type');
573         type = mapGroupmember && type == 'groupmember' ? 'user' : type;
574
575         return type + '-' + this.getUserId();
576     },
577
578     /**
579      * returns true for external contacts
580      */
581     isExternal: function() {
582
583         var isExternal = false,
584             user_type = this.get('user_type');
585         if (user_type == 'user' || user_type == 'groupmember') {
586             isExternal = !this.getUserAccountId();
587         }
588
589         return isExternal;
590     },
591
592     /**
593      * returns account_id if attender is/has a user account
594      * 
595      * @return {String}
596      */
597     getUserAccountId: function() {
598         var user_type = this.get('user_type');
599         if (user_type == 'user' || user_type == 'groupmember') {
600             var user_id = this.get('user_id');
601             if (! user_id) {
602                 return null;
603             }
604             
605             // we expect user_id to be a user or contact object or record
606             if (typeof user_id.get == 'function') {
607                 if (user_id.get('contact_id')) {
608                     // user_id is a account record
609                     return user_id.get('accountId');
610                 } else {
611                     // user_id is a contact record
612                     return user_id.get('account_id');
613                 }
614             } else if (user_id.hasOwnProperty('contact_id')) {
615                 // user_id contains account data
616                 return user_id.accountId;
617             } else if (user_id.hasOwnProperty('account_id')) {
618                 // user_id contains contact data
619                 return user_id.account_id;
620             }
621             
622             // this might happen if contact resolved, due to right restrictions
623             return user_id;
624             
625         }
626         return null;
627     },
628     
629     /**
630      * returns id of attender of any kind
631      */
632     getUserId: function() {
633         var user_id = this.get('user_id');
634         if (! user_id) {
635             return null;
636         }
637         
638         var userData = (typeof user_id.get == 'function') ? user_id.data : user_id;
639
640         if (!userData) {
641             return null;
642         }
643         
644         if (typeof userData != 'object') {
645             return userData;
646         }
647         
648         switch (this.get('user_type')) {
649             case 'user':
650             case 'groupmember':
651             case 'memberOf':
652                 if (userData.hasOwnProperty('contact_id')) {
653                     // userData contains account
654                     return userData.contact_id;
655                 } else if (userData.hasOwnProperty('account_id')) {
656                     // userData contains contact
657                     return userData.id;
658                 } else if (userData.group_id) {
659                     // userData contains list
660                     return userData.id;
661                 } else if (userData.list_id) {
662                     // userData contains group
663                     return userData.list_id;
664                 }
665                 break;
666             default:
667                 return userData.id
668                 break;
669         }
670     },
671
672     getIconCls: function() {
673         var type = this.get('user_type'),
674             cls = 'cal-attendee-type-';
675
676         switch(type) {
677             case 'user':
678                 cls = 'renderer_typeAccountIcon';
679                 break;
680             case 'group':
681                 cls = 'renderer_accountGroupIcon';
682                 break;
683             default:
684                 cls += type;
685                 break;
686         }
687
688         return cls;
689     }
690 });
691
692 /**
693  * @namespace Tine.Calendar.Model
694  * 
695  * get default data for a new attender
696  *  
697  * @return {Object} default data
698  * @static
699  */ 
700 Tine.Calendar.Model.Attender.getDefaultData = function() {
701     return {
702         // @TODO have some config here? user vs. default?
703         user_type: 'any',
704         role: 'REQ',
705         quantity: 1,
706         status: 'NEEDS-ACTION'
707     };
708 };
709
710 /**
711  * @namespace Tine.Calendar.Model
712  * 
713  * get default data for a new resource
714  *  
715  * @return {Object} default data
716  * @static
717  */ 
718 Tine.Calendar.Model.Attender.getDefaultResourceData = function() {
719     return {
720         user_type: 'resource',
721         role: 'REQ',
722         quantity: 1,
723         status: 'NEEDS-ACTION'
724     };
725 };
726
727 /**
728  * @namespace Tine.Calendar.Model
729  * 
730  * creates store of attender objects
731  * 
732  * @param  {Array} attendeeData
733  * @return {Ext.data.Store}
734  * @static
735  */ 
736 Tine.Calendar.Model.Attender.getAttendeeStore = function(attendeeData) {
737     var attendeeStore = new Ext.data.SimpleStore({
738         fields: Tine.Calendar.Model.Attender.getFieldDefinitions(),
739         sortInfo: {field: 'user_id', direction: 'ASC'}
740     });
741
742     if (Ext.isString(attendeeData)) {
743         attendeeData = Ext.decode(attendeeData || null);
744     }
745
746     Ext.each(attendeeData, function(attender) {
747         if (attender) {
748             var record = new Tine.Calendar.Model.Attender(attender, attender.id && Ext.isString(attender.id) ? attender.id : Ext.id());
749             if (record.get('user_id') == "currentContact") {
750                 record.set('user_id', Tine.Tinebase.registry.get('userContact'));
751             }
752             attendeeStore.addSorted(record);
753         }
754     });
755     
756     return attendeeStore;
757 };
758
759 /**
760  * returns attender record of current account if exists, else false
761  * @static
762  */
763 Tine.Calendar.Model.Attender.getAttendeeStore.getMyAttenderRecord = function(attendeeStore) {
764     var currentAccountId = Tine.Tinebase.registry.get('currentAccount').accountId;
765     var myRecord = false;
766
767     attendeeStore.each(function(attender) {
768         var userAccountId = attender.getUserAccountId();
769         if (userAccountId == currentAccountId) {
770             myRecord = attender;
771             return false;
772         }
773     }, this);
774
775     return myRecord;
776 };
777     
778 /**
779  * returns attendee record of given attendee if exists, else false
780  * @static
781  */
782 Tine.Calendar.Model.Attender.getAttendeeStore.getAttenderRecord = function(attendeeStore, attendee) {
783     var attendeeRecord = false;
784
785     if (! Ext.isFunction(attendee.beginEdit)) {
786         attendee = new Tine.Calendar.Model.Attender(attendee, attendee.id);
787     }
788
789     attendeeStore.each(function(r) {
790         var attendeeType = [attendee.get('user_type')];
791
792         // add groupmember for user
793         if (attendeeType[0] == 'user') {
794             attendeeType.push('groupmember');
795         }
796         if (attendeeType[0] == 'groupmember') {
797             attendeeType.push('user');
798         }
799
800         if (attendeeType.indexOf(r.get('user_type')) >= 0 && r.getUserId() == attendee.getUserId()) {
801             attendeeRecord = r;
802             return false;
803         }
804     }, this);
805     
806     return attendeeRecord;
807 };
808
809 Tine.Calendar.Model.Attender.getAttendeeStore.getSignature = function(attendee) {
810     var _ = window.lodash;
811
812     attendee = _.isFunction(attendee.beginEdit) ? attendee.data : attendee;
813     return [attendee.cal_event_id, attendee.user_type, attendee.user_id.id, attendee.role].join('/');
814 };
815
816 Tine.Calendar.Model.Attender.getAttendeeStore.fromSignature = function(signatureId) {
817     var ids = signatureId.split('/');
818
819     return new Tine.Calendar.Model.Attender({
820         cal_event_id: ids[0],
821         user_type: ids[1],
822         user_id: ids[2],
823         role: ids[3]
824     });
825 }
826
827 /**
828  * returns attendee data
829  * optinally fills into event record
830  */
831 Tine.Calendar.Model.Attender.getAttendeeStore.getData = function(attendeeStore, event) {
832     var attendeeData = [];
833
834     Tine.Tinebase.common.assertComparable(attendeeData);
835
836     attendeeStore.each(function (attender) {
837         var user_id = attender.get('user_id');
838         if (user_id/* && user_id.id*/) {
839             if (typeof user_id.get == 'function') {
840                 attender.data.user_id = user_id.data;
841             }
842
843             attendeeData.push(attender.data);
844         }
845     }, this);
846
847     if (event) {
848         event.set('attendee', attendeeData);
849     }
850
851     return attendeeData;
852 };
853
854 // PROXY
855 Tine.Calendar.Model.AttenderProxy = function(config) {
856     Tine.Calendar.Model.AttenderProxy.superclass.constructor.call(this, config);
857     this.jsonReader.readRecords = this.readRecords.createDelegate(this);
858 };
859 Ext.extend(Tine.Calendar.Model.AttenderProxy, Tine.Tinebase.data.RecordProxy, {
860     /**
861      * provide events to do an freeBusy info checkup for when searching attendee
862      *
863      * @cfg {Function} freeBusyEventsProvider
864      */
865     freeBusyEventsProvider: Ext.emptyFn,
866
867     recordClass: Tine.Calendar.Model.Attender,
868
869     searchRecords: function(filter, paging, options) {
870         var _ = window.lodash,
871             fbEvents = _.union([].concat(this.freeBusyEventsProvider()));
872
873         _.set(options, 'params.ignoreUIDs', _.union(_.map(fbEvents, 'data.uid')));
874         _.set(options, 'params.events', _.map(fbEvents, 'data'));
875
876         return Tine.Calendar.Model.AttenderProxy.superclass.searchRecords.apply(this, arguments);
877     },
878
879     readRecords : function(resultData){
880         var _ = window.lodash,
881             totalcount = 0,
882             fbEvents = _.compact([].concat(this.freeBusyEventsProvider())),
883             records = [],
884             fbInfos = _.map(fbEvents, function(fbEvent) {
885                 return new Tine.Calendar.FreeBusyInfo(resultData.freeBusyInfo[fbEvent.get('id')]);
886             });
887
888         _.each(['user', 'group', 'resource'], function(type) {
889             var typeResult = _.get(resultData, type, {}),
890                 typeCount = _.get(typeResult, 'totalcount', 0),
891                 typeData = _.get(typeResult, 'results', []);
892
893             totalcount += +typeCount;
894             _.each(typeData, function(userData) {
895                 var id = type + '-' + userData.id,
896                     attendeeData = _.assign(Tine.Calendar.Model.Attender.getDefaultData(), {
897                         id: id,
898                         user_type: type,
899                         user_id: userData
900                     }),
901                     attendee = new Tine.Calendar.Model.Attender(attendeeData, id);
902
903                 if (fbEvents.length) {
904                     attendee.set('fbInfo', _.map(fbInfos, function(fbInfo, idx) {
905                         return fbInfo.getStateOfAttendee(attendee, fbEvents[idx]);
906                     }).join('<br >'));
907                 }
908                 records.push(attendee);
909             });
910         });
911
912         return {
913             success : true,
914             records: records,
915             totalRecords: totalcount
916         };
917     }
918 });
919
920 /**
921  * @namespace Tine.Calendar.Model
922  * @class Tine.Calendar.Model.Resource
923  * @extends Tine.Tinebase.data.Record
924  * Resource Record Definition
925  */
926 Tine.Calendar.Model.Resource = Tine.Tinebase.data.Record.create(Tine.Tinebase.Model.genericFields.concat([
927     {name: 'id'},
928     {name: 'name'},
929     {name: 'description'},
930     {name: 'email'},
931     {name: 'max_number_of_people', type: 'int'},
932     {name: 'type', type: 'keyField', keyFieldConfigName: 'resourceTypes'},
933     {name: 'status', type: 'keyField', keyFieldConfigName: 'attendeeStatus'},
934     {name: 'busy_type', type: 'keyField', keyFieldConfigName: 'freebusyTypes'},
935     {name: 'suppress_notification', type: 'bool'},
936     {name: 'tags'},
937     {name: 'notes'},
938     {name: 'grants'},
939     { name: 'attachments'},
940     { name: 'relations',   omitDuplicateResolving: true},
941     { name: 'customfields', omitDuplicateResolving: true}
942 ]), {
943     appName: 'Calendar',
944     modelName: 'Resource',
945     idProperty: 'id',
946     titleProperty: 'name',
947     containerProperty: 'container_id',
948     // ngettext('Resource', 'Resources', n); gettext('Resources');
949     recordName: 'Resource',
950     recordsName: 'Resources'
951 });
952
953 Tine.Calendar.Model.Resource.getFilterModel = function() {
954     var app = Tine.Tinebase.appMgr.get('Calendar');
955
956     return [
957         {label: i18n._('Quick Search'), field: 'query', operators: ['contains']},
958         {label: app.i18n._('Name'), field: 'name'},
959         {label: app.i18n._('Email'), field: 'email'},
960         {label: app.i18n._('Description'), field: 'description', operators: ['contains', 'notcontains']},
961         {label: app.i18n._('Maximum number of attendee'), field: 'max_number_of_people'},
962         {
963             label: app.i18n._('Type'),
964             field: 'type',
965             filtertype: 'tine.widget.keyfield.filter',
966             app: app,
967             keyfieldName: 'resourceTypes'
968         },
969         {
970             label: app.i18n._('Default attendee status'),
971             field: 'status',
972             filtertype: 'tine.widget.keyfield.filter',
973             app: app,
974             keyfieldName: 'attendeeStatus'
975         },
976         {
977             label: app.i18n._('Busy Type'),
978             field: 'type',
979             filtertype: 'tine.widget.keyfield.filter',
980             app: app,
981             keyfieldName: 'freebusyTypes'
982         },
983         {filtertype: 'tinebase.tag', app: app}
984     ];
985 };
986
987 /**
988  * @namespace   Tine.Calendar.Model
989  * @class       Tine.Calendar.Model.iMIP
990  * @extends     Tine.Tinebase.data.Record
991  * iMIP Record Definition
992  */
993 Tine.Calendar.Model.iMIP = Tine.Tinebase.data.Record.create([
994     {name: 'id'},
995     {name: 'ics'},
996     {name: 'method'},
997     {name: 'originator'},
998     {name: 'userAgent'},
999     {name: 'event'},
1000     {name: 'existing_event'},
1001     {name: 'preconditions'}
1002 ], {
1003     appName: 'Calendar',
1004     modelName: 'iMIP',
1005     idProperty: 'id'
1006 });