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