]> err.no Git - scalable-opengroupware.org/blob - SOGo/UI/Scheduler/UIxAppointmentEditor.m
added 'weekday' cycle
[scalable-opengroupware.org] / SOGo / UI / Scheduler / UIxAppointmentEditor.m
1 /*
2   Copyright (C) 2004 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
6   OGo is free software; you can redistribute it and/or modify it under
7   the terms of the GNU Lesser General Public License as published by the
8   Free Software Foundation; either version 2, or (at your option) any
9   later version.
10
11   OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12   WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14   License for more details.
15
16   You should have received a copy of the GNU Lesser General Public
17   License along with OGo; see the file COPYING.  If not, write to the
18   Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
19   02111-1307, USA.
20 */
21 // $Id$
22
23 #include <SOGoUI/UIxComponent.h>
24
25 /* TODO: CLEAN UP */
26
27 @class NSString;
28 @class iCalPerson;
29 @class iCalRecurrenceRule;
30 @class SOGoAppointment;
31
32 @interface UIxAppointmentEditor : UIxComponent
33 {
34   NSString *iCalString;
35   NSString *errorText;
36   id item;
37   
38   /* individual values */
39   NSCalendarDate *startDate;
40   NSCalendarDate *endDate;
41   NSCalendarDate *cycleUntilDate;
42   NSString       *title;
43   NSString       *location;
44   NSString       *comment;
45   NSArray        *participants;     /* array of iCalPerson's */
46   NSArray        *resources;        /* array of iCalPerson's */
47   NSString       *priority;
48   NSArray        *categories;
49   NSString       *accessClass;
50   BOOL           isPrivate;         /* default: NO */
51   BOOL           checkForConflicts; /* default: NO */
52   NSDictionary   *cycle;
53 }
54
55 - (NSString *)iCalStringTemplate;
56 - (NSString *)iCalString;
57
58 - (void)setIsPrivate:(BOOL)_yn;
59 - (void)setAccessClass:(NSString *)_class;
60
61 - (void)setCheckForConflicts:(BOOL)_checkForConflicts;
62 - (BOOL)checkForConflicts;
63
64 - (BOOL)hasCycle;
65 - (iCalRecurrenceRule *)rrule;
66 - (void)adjustCycleControlsForRRule:(iCalRecurrenceRule *)_rrule;
67 - (NSDictionary *)cycleMatchingRRule:(iCalRecurrenceRule *)_rrule;
68
69 - (NSString *)_completeURIForMethod:(NSString *)_method;
70
71 - (NSArray *)getICalPersonsFromFormValues:(NSArray *)_values
72   treatAsResource:(BOOL)_isResource;
73
74 - (NSString *)iCalParticipantsAndResourcesStringFromQueryParameters;
75 - (NSString *)iCalParticipantsStringFromQueryParameters;
76 - (NSString *)iCalResourcesStringFromQueryParameters;
77 - (NSString *)iCalStringFromQueryParameter:(NSString *)_qp
78               format:(NSString *)_format;
79 - (NSString *)iCalOrganizerString;
80
81 - (id)acceptOrDeclineAction:(BOOL)_accept;
82
83 @end
84
85 #include "common.h"
86 #include <NGiCal/NGiCal.h>
87 #include <NGExtensions/NGCalendarDateRange.h>
88 #include <SOGoUI/SOGoDateFormatter.h>
89 #include <SOGo/SOGoAppointment.h>
90 #include <SOGo/AgenorUserManager.h>
91 #include <Appointments/SOGoAppointmentFolder.h>
92 #include <Appointments/SOGoAppointmentObject.h>
93 #include "iCalPerson+UIx.h"
94 #include "UIxComponent+Agenor.h"
95
96 @interface iCalRecurrenceRule (SOGoExtensions)
97 - (NSString *)cycleRepresentationForSOGo;
98 @end
99
100 @interface NSDate(UsedPrivates)
101 - (NSString *)icalString; // TODO: this is in NGiCal
102 @end
103
104 @implementation UIxAppointmentEditor
105
106 - (id)init {
107   self = [super init];
108   if(self) {
109     [self setIsPrivate:NO];
110     [self setCheckForConflicts:NO];
111   }
112   return self;
113 }
114
115 - (void)dealloc {
116   [self->iCalString     release];
117   [self->errorText      release];
118   [self->item           release];
119
120   [self->startDate      release];
121   [self->endDate        release];
122   [self->cycleUntilDate release];
123   [self->title          release];
124   [self->location       release];
125   [self->comment        release];
126   [self->participants   release];
127   [self->resources      release];
128   [self->priority       release];
129   [self->categories     release];
130   [self->accessClass    release];
131   [self->cycle          release];
132   [super dealloc];
133 }
134
135 /* accessors */
136
137 - (void)setItem:(id)_item {
138   ASSIGN(self->item, _item);
139 }
140 - (id)item {
141   return self->item;
142 }
143
144 - (void)setErrorText:(NSString *)_txt {
145   ASSIGNCOPY(self->errorText, _txt);
146 }
147 - (NSString *)errorText {
148   return self->errorText;
149 }
150 - (BOOL)hasErrorText {
151   return [self->errorText length] > 0 ? YES : NO;
152 }
153
154 - (NSFormatter *)titleDateFormatter {
155   SOGoDateFormatter *fmt;
156   
157   fmt = [[[SOGoDateFormatter alloc] initWithLocale:[self locale]] autorelease];
158   [fmt setFullWeekdayNameAndDetails];
159   return fmt;
160 }
161
162 - (void)setAptStartDate:(NSCalendarDate *)_date {
163   ASSIGN(self->startDate, _date);
164 }
165 - (NSCalendarDate *)aptStartDate {
166   return self->startDate;
167 }
168 - (void)setAptEndDate:(NSCalendarDate *)_date {
169   ASSIGN(self->endDate, _date);
170 }
171 - (NSCalendarDate *)aptEndDate {
172   return self->endDate;
173 }
174
175 - (void)setTitle:(NSString *)_value {
176   ASSIGNCOPY(self->title, _value);
177 }
178 - (NSString *)title {
179   return self->title;
180 }
181 - (void)setLocation:(NSString *)_value {
182   ASSIGNCOPY(self->location, _value);
183 }
184 - (NSString *)location {
185   return self->location;
186 }
187 - (void)setComment:(NSString *)_value {
188   ASSIGNCOPY(self->comment, _value);
189 }
190 - (NSString *)comment {
191   return self->comment;
192 }
193
194 - (void)setParticipants:(NSArray *)_parts {
195   ASSIGN(self->participants, _parts);
196 }
197 - (NSArray *)participants {
198   return self->participants;
199 }
200 - (void)setResources:(NSArray *)_res {
201   ASSIGN(self->resources, _res);
202 }
203 - (NSArray *)resources {
204   return self->resources;
205 }
206
207 /* priorities */
208
209 - (NSArray *)priorities {
210   /* 0 == undefined
211      5 == normal
212      1 == high
213   */
214   static NSArray *priorities = nil;
215
216   if (!priorities)
217     priorities = [[NSArray arrayWithObjects:@"0", @"5", @"1", nil] retain];
218   return priorities;
219 }
220
221 - (NSString *)itemPriorityText {
222   NSString *key;
223   
224   key = [NSString stringWithFormat:@"prio_%@", self->item];
225   return [self labelForKey:key];
226 }
227
228 - (void)setPriority:(NSString *)_priority {
229   ASSIGN(self->priority, _priority);
230 }
231 - (NSString *)priority {
232   return self->priority;
233 }
234
235
236 /* categories */
237
238 - (NSArray *)categoryItems {
239   // TODO: make this configurable?
240   /*
241    Tasks categories will be modified as follow :
242    â€“ by default (a simple logo or no logo at all),
243    â€“ appointment,
244    â€“ outside,
245    â€“ meeting,
246    â€“ holidays,
247    â€“ phone.
248   */
249   static NSArray *categoryItems = nil;
250   
251   if (!categoryItems) {
252     categoryItems = [[NSArray arrayWithObjects:@"APPOINTMENT",
253                                                @"NOT IN OFFICE",
254                                                @"MEETING",
255                                                @"HOLIDAY",
256                                                @"PHONE CALL",
257                                                nil] retain];
258   }
259   return categoryItems;
260 }
261
262 - (NSString *)itemCategoryText {
263   return [self labelForKey:self->item];
264 }
265
266 - (void)setCategories:(NSArray *)_categories {
267   ASSIGN(self->categories, _categories);
268 }
269 - (NSArray *)categories {
270   return self->categories;
271 }
272
273 /* class */
274
275 #if 0
276 - (NSArray *)accessClassItems {
277   static NSArray classItems = nil;
278   
279   if (!classItems) {
280     return [[NSArray arrayWithObjects:@"PUBLIC", @"PRIVATE", nil] retain];
281   }
282   return classItems;
283 }
284 #endif
285
286 - (void)setAccessClass:(NSString *)_class {
287   ASSIGN(self->accessClass, _class);
288 }
289 - (NSString *)accessClass {
290   return self->accessClass;
291 }
292
293 - (void)setIsPrivate:(BOOL)_yn {
294   if (_yn)
295     [self setAccessClass:@"PRIVATE"];
296   else
297     [self setAccessClass:@"PUBLIC"];
298   self->isPrivate = _yn;
299 }
300 - (BOOL)isPrivate {
301   return self->isPrivate;
302 }
303
304 - (void)setCheckForConflicts:(BOOL)_checkForConflicts {
305   self->checkForConflicts = _checkForConflicts;
306 }
307 - (BOOL)checkForConflicts {
308   return self->checkForConflicts;
309 }
310
311 - (NSArray *)cycles {
312   static NSArray *cycles = nil;
313   
314   if (!cycles) {
315     NSBundle *bundle;
316     NSString *path;
317
318     bundle = [NSBundle bundleForClass:[self class]];
319     path   = [bundle pathForResource:@"cycles" ofType:@"plist"];
320     NSAssert(path != nil, @"Cannot find cycles.plist!");
321     cycles = [[NSArray arrayWithContentsOfFile:path] retain];
322     NSAssert(cycles != nil, @"Cannot instantiate cycles from cycles.plist!");
323   }
324   return cycles;
325 }
326
327 - (void)setCycle:(NSDictionary *)_cycle {
328   ASSIGN(self->cycle, _cycle);
329 }
330 - (NSDictionary *)cycle {
331   return self->cycle;
332 }
333 - (BOOL)hasCycle {
334   [self debugWithFormat:@"cycle: %@", self->cycle];
335   if (![self->cycle objectForKey:@"rule"])
336     return NO;
337   return YES;
338 }
339 - (NSString *)cycleLabel {
340   NSString *key;
341   
342   key = [self->item objectForKey:@"label"];
343   return [self labelForKey:key];
344 }
345
346 - (iCalRecurrenceRule *)rrule {
347   NSString           *ruleRep;
348   iCalRecurrenceRule *rule;
349
350   if (![self hasCycle])
351     return nil;
352   ruleRep = [self->cycle objectForKey:@"rule"];
353   rule    = [iCalRecurrenceRule recurrenceRuleWithICalRepresentation:ruleRep];
354
355   if (self->cycleUntilDate)
356     [rule setUntilDate:self->cycleUntilDate];
357   return rule;
358 }
359
360 - (void)adjustCycleControlsForRRule:(iCalRecurrenceRule *)_rrule {
361   NSDictionary *c;
362   
363   c = [self cycleMatchingRRule:_rrule];
364   [self setCycle:c];
365
366   [self->cycleUntilDate release];
367   self->cycleUntilDate = [[_rrule untilDate] copy];
368   [self->cycleUntilDate setTimeZone:[self viewTimeZone]];
369 }
370
371 /*
372  This method is necessary, because we have a fixed sets of cycles in the UI.
373  The model is able to represent arbitrary rules, however.
374  There SHOULD be a different UI, similar to iCal.app, to allow modelling
375  of more complex rules.
376  
377  This method obviously cannot map all existing rules back to the fixed list
378  in cycles.plist. This should be fixed in a future version when interop
379  becomes more important.
380  */
381 - (NSDictionary *)cycleMatchingRRule:(iCalRecurrenceRule *)_rrule {
382   NSString *cycleRep;
383   NSArray  *cycles;
384   unsigned i, count;
385
386   if (!_rrule)
387     return [[self cycles] objectAtIndex:0];
388
389   cycleRep = [_rrule cycleRepresentationForSOGo];
390   cycles   = [self cycles];
391   count    = [cycles count];
392   for (i = 1; i < count; i++) {
393     NSDictionary *c;
394     NSString     *cr;
395
396     c  = [cycles objectAtIndex:i];
397     cr = [c objectForKey:@"rule"];
398     if ([cr isEqualToString:cycleRep])
399       return c;
400   }
401   [self warnWithFormat:@"No default cycle for rrule found! -> %@", _rrule];
402   return nil;
403 }
404
405
406 /* transparency */
407
408 - (NSString *)transparency {
409   return @"TRANSPARENT";
410 }
411
412
413 /* iCal */
414
415 - (void)setICalString:(NSString *)_s {
416   ASSIGNCOPY(self->iCalString, _s);
417 }
418 - (NSString *)iCalString {
419   return self->iCalString;
420 }
421
422 - (NSString *)iCalStringTemplate {
423   static NSString *iCalStringTemplate = \
424     @"BEGIN:VCALENDAR\r\n"
425     @"METHOD:REQUEST\r\n"
426     @"PRODID:OpenGroupware.org SOGo 0.9\r\n"
427     @"VERSION:2.0\r\n"
428     @"BEGIN:VEVENT\r\n"
429     @"UID:%@\r\n"
430     @"CLASS:PUBLIC\r\n"
431     @"STATUS:CONFIRMED\r\n" /* confirmed by default */
432     @"DTSTAMP:%@\r\n"
433     @"DTSTART:%@\r\n"
434     @"DTEND:%@\r\n"
435     @"TRANSP:%@\r\n"
436     @"SEQUENCE:1\r\n"
437     @"PRIORITY:5\r\n"
438     @"%@"                   /* organizer */
439     @"%@"                   /* participants and resources */
440     @"END:VEVENT\r\n"
441     @"END:VCALENDAR";
442
443   NSCalendarDate *lStartDate, *lEndDate;
444   NSString       *template, *s;
445   unsigned       minutes;
446
447   s = [self queryParameterForKey:@"dur"];
448   if(s && [s length] > 0) {
449     minutes = [s intValue];
450   }
451   else {
452     minutes = 60;
453   }
454   lStartDate = [self selectedDate];
455   lEndDate   = [lStartDate dateByAddingYears:0 months:0 days:0
456                            hours:0 minutes:minutes seconds:0];
457   
458   s          = [self iCalParticipantsAndResourcesStringFromQueryParameters];
459   template   = [NSString stringWithFormat:iCalStringTemplate,
460                                                       [[self clientObject] nameInContainer],
461                                                       [[NSCalendarDate date] icalString],
462                                           [lStartDate icalString],
463                                           [lEndDate icalString],
464                                           [self transparency],
465                                           [self iCalOrganizerString],
466                                           s];
467   return template;
468 }
469
470 - (NSString *)iCalParticipantsAndResourcesStringFromQueryParameters {
471   NSString *s;
472   
473   s = [self iCalParticipantsStringFromQueryParameters];
474   return [s stringByAppendingString:
475             [self iCalResourcesStringFromQueryParameters]];
476 }
477
478 - (NSString *)iCalParticipantsStringFromQueryParameters {
479   static NSString *iCalParticipantString = \
480     @"ATTENDEE;ROLE=REQ-PARTICIPANT;CN=\"%@\":mailto:%@\r\n";
481   
482   return [self iCalStringFromQueryParameter:@"ps"
483                format:iCalParticipantString];
484 }
485
486 - (NSString *)iCalResourcesStringFromQueryParameters {
487   static NSString *iCalResourceString = \
488     @"ATTENDEE;ROLE=NON-PARTICIPANT;CN=\"%@\":mailto:%@\r\n";
489
490   return [self iCalStringFromQueryParameter:@"rs"
491                format:iCalResourceString];
492 }
493
494 - (NSString *)iCalStringFromQueryParameter:(NSString *)_qp
495               format:(NSString *)_format
496 {
497   AgenorUserManager *um;
498   NSMutableString *iCalRep;
499   NSString *s;
500
501   um = [AgenorUserManager sharedUserManager];
502   iCalRep = (NSMutableString *)[NSMutableString string];
503   s = [self queryParameterForKey:_qp];
504   if(s && [s length] > 0) {
505     NSArray *es;
506     unsigned i, count;
507     
508     es = [s componentsSeparatedByString:@","];
509     count = [es count];
510     for(i = 0; i < count; i++) {
511       NSString *email, *cn;
512       
513       email = [es objectAtIndex:i];
514       cn = [um getCNForUID:[um getUIDForEmail:email]];
515       [iCalRep appendFormat:_format, cn, email];
516     }
517   }
518   return iCalRep;
519 }
520
521 - (NSString *)iCalOrganizerString {
522   static NSString *fmt = @"ORGANIZER;CN=\"%@\":mailto:%@\r\n";
523   return [NSString stringWithFormat:fmt,
524                                       [self cnForUser],
525                                       [self emailForUser]];
526 }
527
528 #if 0
529 - (iCalPerson *)getOrganizer {
530   iCalPerson *p;
531   NSString   *emailProp;
532   
533   emailProp = [@"mailto:" stringByAppendingString:[self emailForUser]];
534   p = [[[iCalPerson alloc] init] autorelease];
535   [p setEmail:emailProp];
536   [p setCn:[self cnForUser]];
537   return p;
538 }
539 #endif
540
541
542 /* helper */
543
544 - (NSString *)_completeURIForMethod:(NSString *)_method {
545   NSString *uri;
546   NSRange r;
547     
548   uri = [[[self context] request] uri];
549     
550   /* first: identify query parameters */
551   r = [uri rangeOfString:@"?" options:NSBackwardsSearch];
552   if (r.length > 0)
553     uri = [uri substringToIndex:r.location];
554     
555   /* next: append trailing slash */
556   if (![uri hasSuffix:@"/"])
557     uri = [uri stringByAppendingString:@"/"];
558   
559   /* next: append method */
560   uri = [uri stringByAppendingString:_method];
561     
562   /* next: append query parameters */
563   return [self completeHrefForMethod:uri];
564 }
565
566 /* new */
567
568 - (id)newAction {
569   /*
570     This method creates a unique ID and redirects to the "edit" method on the
571     new ID.
572     It is actually a folder method and should be defined on the folder.
573     
574     Note: 'clientObject' is the SOGoAppointmentFolder!
575           Update: remember that there are group folders as well.
576   */
577   NSString *uri, *objectId, *method;
578
579   objectId = [NSClassFromString(@"SOGoAppointmentFolder")
580                                globallyUniqueObjectId];
581   if ([objectId length] == 0) {
582     return [NSException exceptionWithHTTPStatus:500 /* Internal Error */
583                         reason:@"could not create a unique ID"];
584   }
585
586   method = [NSString stringWithFormat:@"Calendar/%@/edit", objectId];
587   method = [[self userFolderPath] stringByAppendingPathComponent:method];
588
589   /* add all current calendarUIDs as default participants */
590   if ([[self clientObject] respondsToSelector:@selector(calendarUIDs)]) {
591     AgenorUserManager *um;
592     NSArray           *uids;
593     NSMutableArray    *emails;
594     NSString          *ps;
595     unsigned          i, count;
596
597     um     = [AgenorUserManager sharedUserManager];
598     uids   = [[self clientObject] calendarUIDs];
599     count  = [uids count];
600     emails = [NSMutableArray arrayWithCapacity:count];
601     
602     for (i = 0; i < count; i++) {
603       NSString *email;
604       
605       email = [um getEmailForUID:[uids objectAtIndex:i]];
606       if (email)
607         [emails addObject:email];
608     }
609     ps = [emails componentsJoinedByString:@","];
610     [self setQueryParameter:ps forKey:@"ps"];
611   }
612   uri = [self completeHrefForMethod:method];
613   return [self redirectToLocation:uri];
614 }
615
616 /* save */
617
618 /* returned dates are in GMT */
619 - (NSCalendarDate *)_dateFromString:(NSString *)_str {
620   NSCalendarDate *date;
621   
622   date = [NSCalendarDate dateWithString:_str 
623                          calendarFormat:@"%Y-%m-%d %H:%M %Z"];
624   [date setTimeZone:[self backendTimeZone]];
625   return date;
626 }
627
628 - (NSArray *)getICalPersonsFromFormValues:(NSArray *)_values
629   treatAsResource:(BOOL)_isResource
630 {
631   unsigned i, count;
632   NSMutableArray *result;
633
634   count = [_values count];
635   result = [[NSMutableArray alloc] initWithCapacity:count];
636   for (i = 0; i < count; i++) {
637     NSString   *pString, *email, *cn;
638     NSRange    r;
639     iCalPerson *p;
640     
641     pString = [_values objectAtIndex:i];
642     if ([pString length] == 0)
643       continue;
644     
645     /* delimiter between email and cn */
646     r = [pString rangeOfString:@";"];
647     if (r.length > 0) {
648       email = [pString substringToIndex:r.location];
649       cn = (r.location + 1 < [pString length])
650         ? [pString substringFromIndex:r.location + 1]
651         : nil;
652     }
653     else {
654       email = pString;
655       cn    = nil;
656     }
657     if (cn == nil) {
658       /* fallback */
659       AgenorUserManager *um = [AgenorUserManager sharedUserManager];
660       cn = [um getCNForUID:[um getUIDForEmail:email]];
661     }
662     
663     p = [[iCalPerson alloc] init];
664     [p setEmail:[@"mailto:" stringByAppendingString:email]];
665     if ([cn isNotNull]) [p setCn:cn];
666     
667     /* see RFC2445, sect. 4.2.16 for details */
668     [p setRole:_isResource ? @"NON-PARTICIPANT" : @"REQ-PARTICIPANT"];
669     [result addObject:p];
670     [p release];
671   }
672   return [result autorelease];
673 }
674
675 - (BOOL)isWriteableClientObject {
676   return [[self clientObject] 
677                 respondsToSelector:@selector(saveContentString:)];
678 }
679
680 - (void)loadValuesFromAppointment:(SOGoAppointment *)_appointment {
681   NSString           *s;
682   iCalRecurrenceRule *rrule;
683
684   if ((self->startDate = [[_appointment startDate] copy]) == nil)
685     self->startDate = [[[NSCalendarDate date] hour:11 minute:0] copy];
686   if ((self->endDate = [[_appointment endDate] copy]) == nil) {
687     self->endDate =
688       [[self->startDate hour:[self->startDate hourOfDay] + 1 minute:0] copy];
689   }
690   [self->startDate setTimeZone:[self viewTimeZone]];
691   [self->endDate   setTimeZone:[self viewTimeZone]];
692   
693   self->title        = [[_appointment summary]  copy];
694   self->location     = [[_appointment location] copy];
695   self->comment      = [[_appointment comment]  copy];
696   self->priority     = [[_appointment priority] copy];
697   self->categories   = [[_appointment categories]   retain];
698   self->participants = [[_appointment participants] retain];
699   self->resources    = [[_appointment resources]    retain];
700
701   s                  = [_appointment accessClass];
702   if(!s || [s isEqualToString:@"PUBLIC"])
703     [self setIsPrivate:NO];
704   else
705     [self setIsPrivate:YES]; /* we're possibly loosing information here */
706
707   /* cycles */
708   rrule = [_appointment recurrenceRule];
709   [self adjustCycleControlsForRRule:rrule];
710 }
711
712 - (void)saveValuesIntoAppointment:(SOGoAppointment *)_appointment {
713   /* merge in form values */
714   NSArray *attendees, *lResources;
715   
716   [_appointment setStartDate:[self aptStartDate]];
717   [_appointment setEndDate:[self aptEndDate]];
718   
719   [_appointment setSummary:[self title]];
720   [_appointment setLocation:[self location]];
721   [_appointment setComment:[self comment]];
722   [_appointment setPriority:[self priority]];
723   [_appointment setCategories:[self categories]];
724
725   [_appointment setAccessClass:[self accessClass]];
726   [_appointment setTransparency:[self transparency]];
727
728   attendees  = [self participants];
729   lResources = [self resources];
730   if ([lResources count] > 0) {
731     attendees = ([attendees count] > 0)
732       ? [attendees arrayByAddingObjectsFromArray:lResources]
733       : lResources;
734   }
735   [_appointment setAttendees:attendees];
736
737   /* cycles */
738   [_appointment setRecurrenceRule:[self rrule]];
739 }
740
741 - (void)loadValuesFromICalString:(NSString *)_ical {
742   SOGoAppointment *apt;
743
744   apt = [[SOGoAppointment alloc] initWithICalString:_ical];
745   [self loadValuesFromAppointment:apt];
746   [apt release];
747 }
748
749 /* contact editor compatibility */
750
751 - (void)setContentString:(NSString *)_s {
752   [self setICalString:_s];
753 }
754 - (NSString *)contentStringTemplate {
755   return [self iCalStringTemplate];
756 }
757
758 - (void)loadValuesFromContentString:(NSString *)_s {
759   [self loadValuesFromICalString:_s];
760 }
761
762
763 /* access */
764
765 - (BOOL)isMyApt {
766   NSString *owner;
767
768   owner = [[self clientObject] ownerInContext:[self context]];
769   if (!owner)
770     return YES;
771   return [owner isEqualToString:[[self user] login]];
772 }
773
774 - (BOOL)canAccessApt {
775   return [self isMyApt];
776 }
777
778 - (BOOL)canEditApt {
779   return [self isMyApt];
780 }
781
782
783 /* conflict management */
784
785 - (BOOL)containsConflict:(SOGoAppointment *)_apt {
786   NSArray               *attendees, *uids;
787   SOGoAppointmentFolder *groupCalendar;
788   NSArray               *infos;
789   NSArray               *ranges;
790   id                    folder;
791
792   [self logWithFormat:@"search from %@ to %@", 
793           [_apt startDate], [_apt endDate]];
794
795   folder    = [[self clientObject] container];
796   attendees = [_apt attendees];
797   uids      = [folder uidsFromICalPersons:attendees];
798   if ([uids count] == 0) {
799     [self logWithFormat:@"Note: no UIDs selected."];
800     return NO;
801   }
802
803   groupCalendar = [folder lookupGroupCalendarFolderForUIDs:uids
804                           inContext:[self context]];
805   [self debugWithFormat:@"group calendar: %@", groupCalendar];
806   
807   if (![groupCalendar respondsToSelector:@selector(fetchFreebusyInfosFrom:to:)]) {
808     [self errorWithFormat:@"invalid folder to run freebusy query on!"];
809     return NO;
810   }
811
812   infos = [groupCalendar fetchFreebusyInfosFrom:[_apt startDate]
813                          to:[_apt endDate]];
814   [self debugWithFormat:@"  process: %d events", [infos count]];
815
816   ranges = [infos arrayByCreatingDateRangesFromObjectsWithStartDateKey:@"startDate"
817                   andEndDateKey:@"endDate"];
818   ranges = [ranges arrayByCompactingContainedDateRanges];
819   [self debugWithFormat:@"  blocked ranges: %@", ranges];
820
821   return [ranges count] != 0 ? YES : NO;
822 }
823
824
825 /* actions */
826
827 - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{
828   return YES;
829 }
830
831 - (id)testAction {
832   /* for testing only */
833   WORequest       *req;
834   SOGoAppointment *apt;
835   NSString        *content;
836
837   req = [[self context] request];
838   apt = [[SOGoAppointment alloc] initWithICalString:[self iCalString]];
839   [self saveValuesIntoAppointment:apt];
840   content = [apt iCalString];
841   [self logWithFormat:@"%s -- iCal:\n%@",
842     __PRETTY_FUNCTION__,
843     content];
844   [apt release];
845   return self;
846 }
847
848 - (id)defaultAction {
849   NSString *ical;
850   
851   /* load iCalendar file */
852   
853   // TODO: can't we use [clientObject contentAsString]?
854   ical = [[self clientObject] valueForKey:@"iCalString"];
855   if ([ical length] == 0) /* a new appointment */
856     ical = [self contentStringTemplate];
857   
858   [self setContentString:ical];
859   [self loadValuesFromContentString:ical];
860   
861   return self;
862 }
863
864 - (id)saveAction {
865   SOGoAppointment *apt;
866   iCalPerson      *p;
867   NSString        *content;
868   NSException     *ex;
869   
870   if (![self isWriteableClientObject]) {
871     /* return 400 == Bad Request */
872     return [NSException exceptionWithHTTPStatus:400
873                         reason:@"method cannot be invoked on "
874                                @"the specified object"];
875   }
876   
877   apt = [[SOGoAppointment alloc] initWithICalString:[self iCalString]];
878   if (apt == nil) {
879     NSString *s;
880     
881     s = [self labelForKey:@"Invalid iCal data!"];
882     [self setErrorText:s];
883     return self;
884   }
885   
886   [self saveValuesIntoAppointment:apt];
887   p = [apt findParticipantWithEmail:[self emailForUser]];
888   if (p) {
889     [p setParticipationStatus:iCalPersonPartStatAccepted];
890   }
891
892   if ([self checkForConflicts]) {
893     if ([self containsConflict:apt]) {
894       NSString *s;
895       
896       s = [self labelForKey:@"Conflicts found!"];
897       [self setErrorText:s];
898       [apt release];
899       return self;
900     }
901   }
902   content = [apt iCalString];
903   [apt release]; apt = nil;
904   
905   if (content == nil) {
906     NSString *s;
907     
908     s = [self labelForKey:@"Could not create iCal data!"];
909     [self setErrorText:s];
910     return self;
911   }
912   
913   ex = [[self clientObject] saveContentString:content];
914   if (ex != nil) {
915     [self setErrorText:[ex reason]];
916     return self;
917   }
918   
919   return [self redirectToLocation:[self _completeURIForMethod:@".."]];
920 }
921
922 - (id)acceptAction {
923   return [self acceptOrDeclineAction:YES];
924 }
925
926 - (id)declineAction {
927   return [self acceptOrDeclineAction:NO];
928 }
929
930 - (id)acceptOrDeclineAction:(BOOL)_accept {
931   SOGoAppointment *apt;
932   iCalPerson      *p;
933   NSString        *iCal, *content;
934   NSException     *ex;
935
936   if (![self isWriteableClientObject]) {
937     /* 400 == Bad Request */
938     return [NSException exceptionWithHTTPStatus:400
939                         reason:@"method cannot be invoked on "
940                                @"the specified object"];
941   }
942   iCal = [[self clientObject] valueForKey:@"iCalString"];
943   apt  = [[SOGoAppointment alloc] initWithICalString:iCal];
944   if (apt == nil) {
945     /* 500 == Internal Server Error */
946     return [NSException exceptionWithHTTPStatus:500
947                         reason:@"unable to parse appointment"];
948   }
949   
950   p = [apt findParticipantWithEmail:[self emailForUser]];
951   if (!p) {
952     /* 404 == Not found */
953     return [NSException exceptionWithHTTPStatus:404
954                         reason:@"user does not participate in this "
955                                @"appointment"];
956   }
957   if(_accept)
958     [p setParticipationStatus:iCalPersonPartStatAccepted];
959   else
960     [p setParticipationStatus:iCalPersonPartStatDeclined];
961
962   content = [apt iCalString];
963   [apt release];
964   
965   if (content == nil) {
966     /* 500 == Internal Server Error */
967     return [NSException exceptionWithHTTPStatus:500
968                         reason:@"Could not create iCalendar data ..."];
969   }
970   
971   ex = [[self clientObject] saveContentString:content];
972   if (ex != nil) {
973     /* 500 == Internal Server Error */
974     return [NSException exceptionWithHTTPStatus:500
975                         reason:[ex reason]];
976   }
977   
978   return [self redirectToLocation:[self _completeURIForMethod:@".."]];
979 }
980
981 @end /* UIxAppointmentEditor */
982
983
984 /* HACK ALERT
985    This is a pretty ugly (unfortunately necessary) hack to map our limited
986    set of recurrence rules back to the popup list
987 */
988 @interface iCalRecurrenceRule (UsedPrivates)
989 - (NSString *)freq;
990 - (NSString *)byDayList;
991 @end /* iCalRecurrenceRule (UsedPrivates) */
992
993 @implementation iCalRecurrenceRule (SOGoExtensions)
994
995 - (NSString *)cycleRepresentationForSOGo {
996   NSMutableString *s;
997   
998   s = [NSMutableString stringWithCapacity:20];
999   [s appendString:@"FREQ="];
1000   [s appendString:[self freq]];
1001   if ([self repeatInterval] != 1) {
1002     [s appendFormat:@";INTERVAL=%d", [self repeatInterval]];
1003   }
1004   if (self->byDay.mask != 0) {
1005     [s appendString:@";BYDAY="];
1006     [s appendString:[self byDayList]];
1007   }
1008   return s;
1009 }
1010
1011 @end /* iCalRecurrenceRule (SOGoExtensions) */