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