]> err.no Git - scalable-opengroupware.org/blob - UI/Scheduler/UIxComponentEditor.m
initial sync
[scalable-opengroupware.org] / UI / Scheduler / UIxComponentEditor.m
1 /* UIxComponentEditor.m - this file is part of SOGo
2  *
3  * Copyright (C) 2006 Inverse groupe conseil
4  *
5  * Author: Wolfgang Sourdeau <wsourdeau@inverse.ca>
6  *
7  * This file is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2, or (at your option)
10  * any later version.
11  *
12  * This file is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; see the file COPYING.  If not, write to
19  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
20  * Boston, MA 02111-1307, USA.
21  */
22
23 #import <Foundation/NSArray.h>
24 #import <Foundation/NSBundle.h>
25 #import <Foundation/NSException.h>
26 #import <Foundation/NSCalendarDate.h>
27 #import <Foundation/NSString.h>
28 #import <Foundation/NSUserDefaults.h>
29 #import <Foundation/NSURL.h>
30
31 #import <NGCards/iCalPerson.h>
32 #import <NGCards/iCalRepeatableEntityObject.h>
33 #import <NGCards/iCalRecurrenceRule.h>
34 #import <NGCards/NSString+NGCards.h>
35 #import <NGCards/NSCalendarDate+NGCards.h>
36 #import <NGExtensions/NSCalendarDate+misc.h>
37 #import <NGExtensions/NSObject+Logs.h>
38 #import <NGExtensions/NSString+misc.h>
39 #import <NGObjWeb/NSException+HTTP.h>
40 #import <NGObjWeb/WORequest.h>
41
42 #import <SOGo/AgenorUserManager.h>
43 #import <SOGo/SOGoUser.h>
44 #import <SOGoUI/SOGoDateFormatter.h>
45 #import <SoObjects/Appointments/SOGoAppointmentObject.h>
46 #import <SoObjects/Appointments/SOGoTaskObject.h>
47
48 #import "UIxComponent+Agenor.h"
49
50 #import "UIxComponentEditor.h"
51
52 @implementation UIxComponentEditor
53
54 - (id) init
55 {
56   if ((self = [super init]))
57     {
58       [self setPrivacy: @"PUBLIC"];
59       [self setCheckForConflicts: NO];
60       [self setIsCycleEndNever];
61       componentOwner = @"";
62       componentLoaded = NO;
63     }
64
65   return self;
66 }
67
68 - (void) dealloc
69 {
70   [iCalString release];
71   [errorText release];
72   [item release];
73   [startDate release];
74   [cycleUntilDate release];
75   [title release];
76   [location release];
77   [organizer release];
78   [comment release];
79   [participants release];
80   [resources release];
81   [priority release];
82   [categories release];
83   [cycle release];
84   [cycleEnd release];
85   [url release];
86
87   [super dealloc];
88 }
89
90 /* accessors */
91
92 - (void) setItem: (id) _item
93 {
94   ASSIGN(item, _item);
95 }
96
97 - (id) item
98 {
99   return item;
100 }
101
102 - (NSString *) itemPriorityText
103 {
104   return [self labelForKey: [NSString stringWithFormat: @"prio_%@", item]];
105 }
106
107 - (NSString *) itemPrivacyText
108 {
109   return [self labelForKey: [NSString stringWithFormat: @"privacy_%@", item]];
110 }
111
112 - (NSString *) itemStatusText
113 {
114   return [self labelForKey: [NSString stringWithFormat: @"status_%@", item]];
115 }
116
117 - (void) setErrorText: (NSString *) _txt
118 {
119   ASSIGNCOPY(errorText, _txt);
120 }
121
122 - (NSString *) errorText
123 {
124   return errorText;
125 }
126
127 - (BOOL) hasErrorText
128 {
129   return [errorText length] > 0 ? YES : NO;
130 }
131
132 - (void) setStartDate: (NSCalendarDate *) _date
133 {
134   ASSIGN(startDate, _date);
135 }
136
137 - (NSCalendarDate *) startDate
138 {
139   return startDate;
140 }
141
142 - (void) setTitle: (NSString *) _value
143 {
144   ASSIGNCOPY(title, _value);
145 }
146
147 - (NSString *) title
148 {
149   return title;
150 }
151
152 - (void) setUrl: (NSString *) _url
153 {
154   ASSIGNCOPY(url, _url);
155 }
156
157 - (NSString *) url
158 {
159   return url;
160 }
161
162 - (void) setLocation: (NSString *) _value
163 {
164   ASSIGNCOPY(location, _value);
165 }
166
167 - (NSString *) location
168 {
169   return location;
170 }
171
172 - (void) setComment: (NSString *) _value
173 {
174   ASSIGNCOPY(comment, _value);
175 }
176
177 - (NSString *) comment
178 {
179   return comment;
180 }
181
182 - (NSArray *) categoryItems
183 {
184   // TODO: make this configurable?
185   /*
186    Tasks categories will be modified as follow :
187    – by default (a simple logo or no logo at all),
188    – task,
189    – outside,
190    – meeting,
191    – holidays,
192    – phone.
193   */
194   static NSArray *categoryItems = nil;
195   
196   if (!categoryItems)
197     {
198       categoryItems = [NSArray arrayWithObjects: @"APPOINTMENT",
199                                @"NOT IN OFFICE",
200                                @"MEETING",
201                                @"HOLIDAY",
202                                @"PHONE CALL",
203                                nil];
204       [categoryItems retain];
205     }
206
207   return categoryItems;
208 }
209
210 - (void) setCategories: (NSArray *) _categories
211 {
212   ASSIGN(categories, _categories);
213 }
214
215 - (NSArray *) categories
216 {
217   return categories;
218 }
219
220 - (NSString *) itemCategoryText
221 {
222   return [[self labelForKey: item] stringByEscapingHTMLString];
223 }
224
225 /* priorities */
226
227 - (NSArray *) priorities
228 {
229   /* 0 == undefined
230      5 == normal
231      1 == high
232   */
233   static NSArray *priorities = nil;
234
235   if (!priorities)
236     {
237       priorities = [NSArray arrayWithObjects:@"0", @"5", @"1", nil];
238       [priorities retain];
239     }
240
241   return priorities;
242 }
243
244 - (void) setPriority: (NSString *) _priority
245 {
246   ASSIGN(priority, _priority);
247 }
248
249 - (NSString *) priority
250 {
251   return priority;
252 }
253
254 - (NSArray *) privacyClasses
255 {
256   static NSArray *priorities = nil;
257
258   if (!priorities)
259     {
260       priorities = [NSArray arrayWithObjects: @"PUBLIC",
261                             @"CONFIDENTIAL", @"PRIVATE", nil];
262       [priorities retain];
263     }
264
265   return priorities;
266 }
267
268 - (void) setPrivacy: (NSString *) _privacy
269 {
270   ASSIGN(privacy, _privacy);
271 }
272
273 - (NSString *) privacy
274 {
275   return privacy;
276 }
277
278 - (NSArray *) statusTypes
279 {
280   static NSArray *priorities = nil;
281
282   if (!priorities)
283     {
284       priorities = [NSArray arrayWithObjects: @"", @"TENTATIVE", @"CONFIRMED", @"CANCELLED", nil];
285       [priorities retain];
286     }
287
288   return priorities;
289 }
290
291 - (void) setStatus: (NSString *) _status
292 {
293   ASSIGN(status, _status);
294 }
295
296 - (NSString *) status
297 {
298   return status;
299 }
300
301 - (void) setParticipants: (NSArray *) _parts
302 {
303   ASSIGN(participants, _parts);
304 }
305
306 - (NSArray *) participants
307 {
308   return participants;
309 }
310
311 - (void) setResources: (NSArray *) _res
312 {
313   ASSIGN(resources, _res);
314 }
315
316 - (NSArray *) resources
317 {
318   return resources;
319 }
320
321 - (void) setCheckForConflicts: (BOOL) _checkForConflicts
322 {
323   checkForConflicts = _checkForConflicts;
324 }
325
326 - (BOOL) checkForConflicts
327 {
328   return checkForConflicts;
329 }
330
331 - (NSArray *) cycles
332 {
333   NSBundle *bundle;
334   NSString *path;
335   static NSArray *cycles = nil;
336   
337   if (!cycles)
338     {
339       bundle = [NSBundle bundleForClass:[self class]];
340       path   = [bundle pathForResource:@"cycles" ofType:@"plist"];
341       NSAssert(path != nil, @"Cannot find cycles.plist!");
342       cycles = [[NSArray arrayWithContentsOfFile:path] retain];
343       NSAssert(cycles != nil, @"Cannot instantiate cycles from cycles.plist!");
344     }
345
346   return cycles;
347 }
348
349 - (void) setCycle: (NSDictionary *) _cycle
350 {
351   ASSIGN(cycle, _cycle);
352 }
353
354 - (NSDictionary *) cycle
355 {
356   return cycle;
357 }
358
359 - (BOOL) hasCycle
360 {
361   return ([cycle objectForKey: @"rule"] != nil);
362 }
363
364 - (NSString *) cycleLabel
365 {
366   NSString *key;
367   
368   key = [(NSDictionary *)item objectForKey:@"label"];
369
370   return [self labelForKey:key];
371 }
372
373 - (void) setCycleUntilDate: (NSCalendarDate *) _cycleUntilDate
374 {
375   NSCalendarDate *until;
376
377   /* copy hour/minute/second from startDate */
378   until = [_cycleUntilDate hour: [startDate hourOfDay]
379                            minute: [startDate minuteOfHour]
380                            second: [startDate secondOfMinute]];
381   [until setTimeZone: [startDate timeZone]];
382   ASSIGN(cycleUntilDate, until);
383 }
384
385 - (NSCalendarDate *) cycleUntilDate
386 {
387   return cycleUntilDate;
388 }
389
390 - (iCalRecurrenceRule *) rrule
391 {
392   NSString *ruleRep;
393   iCalRecurrenceRule *rule;
394
395   if (![self hasCycle])
396     return nil;
397   ruleRep = [cycle objectForKey:@"rule"];
398   rule = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:ruleRep];
399
400   if (cycleUntilDate && [self isCycleEndUntil])
401     [rule setUntilDate:cycleUntilDate];
402
403   return rule;
404 }
405
406 - (void) adjustCycleControlsForRRule: (iCalRecurrenceRule *) _rrule
407 {
408   NSDictionary *c;
409   NSCalendarDate *until;
410   
411   c = [self cycleMatchingRRule:_rrule];
412   [self setCycle:c];
413
414   until = [[[_rrule untilDate] copy] autorelease];
415   if (!until)
416     until = startDate;
417   else
418     [self setIsCycleEndUntil];
419
420   [until setTimeZone:[[self clientObject] userTimeZone]];
421   [self setCycleUntilDate:until];
422 }
423
424 /*
425  This method is necessary, because we have a fixed sets of cycles in the UI.
426  The model is able to represent arbitrary rules, however.
427  There SHOULD be a different UI, similar to iCal.app, to allow modelling
428  of more complex rules.
429  
430  This method obviously cannot map all existing rules back to the fixed list
431  in cycles.plist. This should be fixed in a future version when interop
432  becomes more important.
433  */
434 - (NSDictionary *) cycleMatchingRRule: (iCalRecurrenceRule *) _rrule
435 {
436   NSString *cycleRep;
437   NSArray *cycles;
438   unsigned i, count;
439
440   if (!_rrule)
441     return [[self cycles] objectAtIndex:0];
442
443   cycleRep = [_rrule versitString];
444   cycles   = [self cycles];
445   count    = [cycles count];
446   for (i = 1; i < count; i++) {
447     NSDictionary *c;
448     NSString *cr;
449
450     c  = [cycles objectAtIndex:i];
451     cr = [c objectForKey:@"rule"];
452     if ([cr isEqualToString:cycleRep])
453       return c;
454   }
455   [self warnWithFormat:@"No default cycle for rrule found! -> %@", _rrule];
456   return nil;
457 }
458
459 /* cycle "ends" - supposed to be 'never', 'COUNT' or 'UNTIL' */
460 - (NSArray *) cycleEnds
461 {
462   static NSArray *ends = nil;
463   
464   if (!ends)
465     {
466       ends = [NSArray arrayWithObjects: @"cycle_end_never",
467                       @"cycle_end_until", nil];
468       [ends retain];
469     }
470
471   return ends;
472 }
473
474 - (void) setCycleEnd: (NSString *) _cycleEnd
475 {
476   ASSIGNCOPY(cycleEnd, _cycleEnd);
477 }
478
479 - (NSString *) cycleEnd
480 {
481   return cycleEnd;
482 }
483
484 - (BOOL) isCycleEndUntil
485 {
486   return (cycleEnd &&
487           [cycleEnd isEqualToString:@"cycle_end_until"]);
488 }
489
490 - (void) setIsCycleEndUntil
491 {
492   [self setCycleEnd:@"cycle_end_until"];
493 }
494
495 - (void) setIsCycleEndNever
496 {
497   [self setCycleEnd:@"cycle_end_never"];
498 }
499
500 /* helpers */
501 - (NSFormatter *) titleDateFormatter
502 {
503   SOGoDateFormatter *fmt;
504   
505   fmt = [[SOGoDateFormatter alloc] initWithLocale: [self locale]];
506   [fmt autorelease];
507   [fmt setFullWeekdayNameAndDetails];
508
509   return fmt;
510 }
511
512 - (NSString *) completeURIForMethod: (NSString *) _method
513 {
514   NSString *uri;
515   NSRange r;
516     
517   uri = [[[self context] request] uri];
518     
519   /* first: identify query parameters */
520   r = [uri rangeOfString:@"?" options:NSBackwardsSearch];
521   if (r.length > 0)
522     uri = [uri substringToIndex:r.location];
523     
524   /* next: append trailing slash */
525   if (![uri hasSuffix:@"/"])
526     uri = [uri stringByAppendingString:@"/"];
527   
528   /* next: append method */
529   uri = [uri stringByAppendingString:_method];
530     
531   /* next: append query parameters */
532   return [self completeHrefForMethod:uri];
533 }
534
535 - (BOOL) isWriteableClientObject
536 {
537   return [[self clientObject] 
538                 respondsToSelector:@selector(saveContentString:)];
539 }
540
541 - (BOOL) shouldTakeValuesFromRequest: (WORequest *) _rq
542                            inContext: (WOContext*) _c
543 {
544   return YES;
545 }
546
547 - (BOOL) containsConflict: (id) _component
548 {
549   [self subclassResponsibility: _cmd];
550
551   return NO;
552 }
553
554 /* access */
555
556 #if 0
557 - (iCalPerson *) getOrganizer
558 {
559   iCalPerson *p;
560   NSString *emailProp;
561   
562   emailProp = [@"MAILTO:" stringByAppendingString:[self emailForUser]];
563   p = [[[iCalPerson alloc] init] autorelease];
564   [p setEmail:emailProp];
565   [p setCn:[self cnForUser]];
566   return p;
567 }
568 #endif
569
570 - (BOOL) isMyComponent
571 {
572   // TODO: this should check a set of emails against the SoUser
573   return ([[organizer rfc822Email] isEqualToString: [self emailForUser]]);
574 }
575
576 - (BOOL) canEditComponent
577 {
578   return [self isMyComponent];
579 }
580
581 /* response generation */
582
583 - (NSString *) initialCycleVisibility
584 {
585   return ([self hasCycle]
586           ? @"visibility: visible;"
587           : @"visibility: hidden;");
588 }
589
590 - (NSString *) initialCycleEndUntilVisibility {
591   return ([self isCycleEndUntil]
592           ? @"visibility: visible;"
593           : @"visibility: hidden;");
594 }
595
596 /* subclasses */
597 - (NSCalendarDate *) newStartDate
598 {
599   NSCalendarDate *newStartDate, *now;
600   int hour;
601
602   newStartDate = [self selectedDate];
603   if ([[self queryParameterForKey: @"hm"] length] == 0)
604     {
605       now = [NSCalendarDate calendarDate];
606       [now setTimeZone: [[self clientObject] userTimeZone]];
607       if (!([[now hour: 8 minute: 0] earlierDate: newStartDate] == newStartDate))
608         {
609           hour = [now hourOfDay];
610           if (hour < 8)
611             newStartDate = [now hour: 8 minute: 0];
612           else if (hour > 18)
613             newStartDate = [[now tomorrow] hour: 8 minute: 0];
614           else
615             newStartDate = now;
616         }
617     }
618
619   return newStartDate;
620 }
621
622 - (void) loadValuesFromComponent: (iCalRepeatableEntityObject *) component
623 {
624   iCalRecurrenceRule *rrule;
625   NSTimeZone *uTZ;
626   SOGoObject *co;
627
628   co = [self clientObject];
629   componentOwner = [co ownerInContext: nil];
630   componentLoaded = YES;
631
632   startDate = [component startDate];
633 //   if ((startDate = [component startDate]) == nil)
634 //     startDate = [[NSCalendarDate date] hour:11 minute:0];
635   uTZ = [co userTimeZone];
636   if (startDate)
637     {
638       [startDate setTimeZone: uTZ];
639       [startDate retain];
640     }
641
642   title        = [[component summary] copy];
643   location     = [[component location] copy];
644   comment      = [[component comment] copy];
645   url          = [[[component url] absoluteString] copy];
646   privacy      = [[component accessClass] copy];
647   priority     = [[component priority] copy];
648   status       = [[component status] copy];
649   categories   = [[[component categories] commaSeparatedValues] retain];
650   organizer    = [[component organizer] retain];
651   participants = [[component participants] retain];
652   resources    = [[component resources] retain];
653
654   /* cycles */
655   if ([component isRecurrent])
656     {
657       rrule = [[component recurrenceRules] objectAtIndex: 0];
658       [self adjustCycleControlsForRRule: rrule];
659     }
660 }
661
662 - (NSString *) iCalStringTemplate
663 {
664   [self subclassResponsibility: _cmd];
665
666   return @"";
667 }
668
669 - (NSString *) iCalParticipantsAndResourcesStringFromQueryParameters
670 {
671   NSString *s;
672   
673   s = [self iCalParticipantsStringFromQueryParameters];
674   return [s stringByAppendingString:
675               [self iCalResourcesStringFromQueryParameters]];
676 }
677
678 - (NSString *) iCalParticipantsStringFromQueryParameters
679 {
680   static NSString *iCalParticipantString = \
681     @"ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN=\"%@\":MAILTO:%@\r\n";
682   
683   return [self iCalStringFromQueryParameter: @"ps"
684                format: iCalParticipantString];
685 }
686
687 - (NSString *) iCalResourcesStringFromQueryParameters
688 {
689   static NSString *iCalResourceString = \
690     @"ATTENDEE;ROLE=NON-PARTICIPANT;CN=\"%@\":MAILTO:%@\r\n";
691
692   return [self iCalStringFromQueryParameter: @"rs"
693                format: iCalResourceString];
694 }
695
696 - (NSString *) iCalStringFromQueryParameter: (NSString *) _qp
697                                      format: (NSString *) _format
698 {
699   AgenorUserManager *um;
700   NSMutableString *iCalRep;
701   NSString *s;
702
703   um = [AgenorUserManager sharedUserManager];
704   iCalRep = (NSMutableString *)[NSMutableString string];
705   s = [self queryParameterForKey:_qp];
706   if(s && [s length] > 0) {
707     NSArray *es;
708     unsigned i, count;
709     
710     es = [s componentsSeparatedByString:@","];
711     count = [es count];
712     for(i = 0; i < count; i++) {
713       NSString *email, *cn;
714       
715       email = [es objectAtIndex:i];
716       cn = [um getCNForUID:[um getUIDForEmail:email]];
717       [iCalRep appendFormat:_format, cn, email];
718     }
719   }
720   return iCalRep;
721 }
722
723 - (NSString *) iCalOrganizerString
724 {
725   return [NSString stringWithFormat: @"ORGANIZER;CN=\"%@\":MAILTO:%@\r\n",
726                    [self cnForUser], [self emailForUser]];
727 }
728
729 - (NSString *) saveUrl
730 {
731   [self subclassResponsibility: _cmd];
732
733   return @"";
734 }
735
736 - (NSException *) validateObjectForStatusChange
737 {
738   id co;
739
740   co = [self clientObject];
741   if (![co
742          respondsToSelector: @selector(changeParticipationStatus:inContext:)])
743     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
744                         reason:
745                           @"method cannot be invoked on the specified object"];
746
747   return nil;
748 }
749
750 /* contact editor compatibility */
751
752 - (void) setICalString: (NSString *) _s
753 {
754   ASSIGNCOPY(iCalString, _s);
755 }
756
757 - (NSString *) iCalString
758 {
759   return iCalString;
760 }
761
762
763 - (NSArray *) availableCalendars
764 {
765   NSEnumerator *rawContacts;
766   NSString *list, *currentId;
767   NSMutableArray *calendars;
768   SOGoUser *user;
769
770   calendars = [NSMutableArray array];
771
772   user = [context activeUser];
773   list = [[user userDefaults] stringForKey: @"calendaruids"];
774   if ([list length] == 0)
775     list = [self shortUserNameForDisplay];
776
777   rawContacts
778     = [[list componentsSeparatedByString: @","] objectEnumerator];
779   currentId = [rawContacts nextObject];
780   while (currentId)
781     {
782       if ([currentId hasPrefix: @"-"])
783         [calendars addObject: [currentId substringFromIndex: 1]];
784       else
785         [calendars addObject: currentId];
786       currentId = [rawContacts nextObject];
787     }
788
789   return calendars;
790 }
791
792 - (NSString *) componentOwner
793 {
794   return componentOwner;
795 }
796
797 - (NSString *) urlButtonClasses
798 {
799   NSString *classes;
800
801   if ([url length])
802     classes = @"button";
803   else
804     classes = @"button _disabled";
805
806   return classes;
807 }
808
809 - (NSString *) _toolbarForCalObject: (iCalEntityObject *) calObject
810 {
811   NSString *filename, *myEmail;
812   iCalPerson *person;
813   NSEnumerator *persons;
814   iCalPersonPartStat myParticipationStatus;
815   BOOL found;
816
817   myEmail = [[[self context] activeUser] email];
818   if ([[organizer rfc822Email] isEqualToString: myEmail])
819     filename = @"SOGoAppointmentObject.toolbar";
820   else
821     {
822       filename = @"";
823       found = NO;
824       persons = [participants objectEnumerator];
825       person = [persons nextObject];
826       while (person && !found)
827         if ([[person rfc822Email] isEqualToString: myEmail])
828           {
829             found = YES;
830             myParticipationStatus = [person participationStatus];
831             if (myParticipationStatus == iCalPersonPartStatAccepted)
832               filename = @"SOGoAppointmentObjectDecline.toolbar";
833             else if (myParticipationStatus == iCalPersonPartStatDeclined)
834               filename = @"SOGoAppointmentObjectAccept.toolbar";
835             else
836               filename = @"SOGoAppointmentObjectAcceptOrDecline.toolbar";
837           }
838         else
839           person = [persons nextObject];
840     }
841
842   return filename;
843 }
844
845 - (NSString *) toolbar
846 {
847   NSString *filename;
848   iCalEntityObject *calObject;
849   id co;
850
851   if (componentLoaded)
852     {
853       co = [self clientObject];
854       if ([co isKindOfClass: [SOGoAppointmentObject class]])
855         {
856           calObject = (iCalEntityObject *) [co event];
857           filename = [self _toolbarForCalObject: calObject];
858         }
859       else if ([co isKindOfClass: [SOGoTaskObject class]])
860         {
861           calObject = (iCalEntityObject *) [co task];
862           filename = [self _toolbarForCalObject: calObject];
863         }
864       else
865         filename = @"";
866     }
867   else
868     filename = @"";
869
870   return filename;
871 }
872
873 @end