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