]> err.no Git - scalable-opengroupware.org/blob - UI/Scheduler/UIxAppointmentEditor.m
some more work on the Kolab viewers
[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 - (void)loadValuesFromAppointment:(SOGoAppointment *)_appointment {
753   NSString           *s;
754   iCalRecurrenceRule *rrule;
755
756   if ((self->startDate = [[_appointment startDate] copy]) == nil)
757     self->startDate = [[[NSCalendarDate date] hour:11 minute:0] copy];
758   if ((self->endDate = [[_appointment endDate] copy]) == nil) {
759     self->endDate =
760       [[self->startDate hour:[self->startDate hourOfDay] + 1 minute:0] copy];
761   }
762   [self->startDate setTimeZone:[self viewTimeZone]];
763   [self->endDate   setTimeZone:[self viewTimeZone]];
764   
765   self->title        = [[_appointment summary]  copy];
766   self->location     = [[_appointment location] copy];
767   self->comment      = [[_appointment comment]  copy];
768   self->priority     = [[_appointment priority] copy];
769   self->categories   = [[_appointment categories]   retain];
770   self->organizer    = [[_appointment organizer]    retain];
771   self->participants = [[_appointment participants] retain];
772   self->resources    = [[_appointment resources]    retain];
773
774   s                  = [_appointment accessClass];
775   if(!s || [s isEqualToString:@"PUBLIC"])
776     [self setIsPrivate:NO];
777   else
778     [self setIsPrivate:YES]; /* we're possibly loosing information here */
779
780   /* cycles */
781   rrule = [_appointment recurrenceRule];
782   [self adjustCycleControlsForRRule:rrule];
783 }
784
785 - (void)saveValuesIntoAppointment:(SOGoAppointment *)_appointment {
786   /* merge in form values */
787   NSArray *attendees, *lResources;
788   
789   [_appointment setStartDate:[self aptStartDate]];
790   [_appointment setEndDate:[self aptEndDate]];
791   
792   [_appointment setSummary:[self title]];
793   [_appointment setLocation:[self location]];
794   [_appointment setComment:[self comment]];
795   [_appointment setPriority:[self priority]];
796   [_appointment setCategories:[self categories]];
797
798   [_appointment setAccessClass:[self accessClass]];
799   [_appointment setTransparency:[self transparency]];
800
801 #if 0
802   /*
803     Note: bad, bad, bad!
804     Organizer is no form value, thus we MUST NOT change it
805   */
806   [_appointment setOrganizer:self->organizer];
807 #endif
808   attendees  = [self participants];
809   lResources = [self resources];
810   if ([lResources count] > 0) {
811     attendees = ([attendees count] > 0)
812       ? [attendees arrayByAddingObjectsFromArray:lResources]
813       : lResources;
814   }
815   [_appointment setAttendees:attendees];
816
817   /* cycles */
818   [_appointment setRecurrenceRule:[self rrule]];
819 }
820
821 - (void)loadValuesFromICalString:(NSString *)_ical {
822   SOGoAppointment *apt;
823
824   apt = [[SOGoAppointment alloc] initWithICalString:_ical];
825   [self loadValuesFromAppointment:apt];
826   [apt release];
827 }
828
829 /* contact editor compatibility */
830
831 - (void)setContentString:(NSString *)_s {
832   [self setICalString:_s];
833 }
834 - (NSString *)contentStringTemplate {
835   return [self iCalStringTemplate];
836 }
837
838 - (void)loadValuesFromContentString:(NSString *)_s {
839   [self loadValuesFromICalString:_s];
840 }
841
842
843 /* access */
844
845 - (BOOL)isMyApt {
846   if (self->organizer == nil)
847     return YES; // assume this is safe to do, right?
848   
849   // TODO: this should check a set of emails against the SoUser
850   return [[self->organizer rfc822Email] isEqualToString:[self emailForUser]];
851 }
852
853 - (BOOL)canAccessApt {
854   return [self isMyApt];
855 }
856
857 - (BOOL)canEditApt {
858   return [self isMyApt];
859 }
860
861
862 /* conflict management */
863
864 - (BOOL)containsConflict:(SOGoAppointment *)_apt {
865   NSArray               *attendees, *uids;
866   SOGoAppointmentFolder *groupCalendar;
867   NSArray               *infos;
868   NSArray               *ranges;
869   id                    folder;
870
871   [self logWithFormat:@"search from %@ to %@", 
872           [_apt startDate], [_apt endDate]];
873
874   folder    = [[self clientObject] container];
875   attendees = [_apt attendees];
876   uids      = [folder uidsFromICalPersons:attendees];
877   if ([uids count] == 0) {
878     [self logWithFormat:@"Note: no UIDs selected."];
879     return NO;
880   }
881
882   groupCalendar = [folder lookupGroupCalendarFolderForUIDs:uids
883                           inContext:[self context]];
884   [self debugWithFormat:@"group calendar: %@", groupCalendar];
885   
886   if (![groupCalendar respondsToSelector:@selector(fetchFreebusyInfosFrom:to:)]) {
887     [self errorWithFormat:@"invalid folder to run freebusy query on!"];
888     return NO;
889   }
890
891   infos = [groupCalendar fetchFreebusyInfosFrom:[_apt startDate]
892                          to:[_apt endDate]];
893   [self debugWithFormat:@"  process: %d events", [infos count]];
894
895   ranges = [infos arrayByCreatingDateRangesFromObjectsWithStartDateKey:@"startDate"
896                   andEndDateKey:@"endDate"];
897   ranges = [ranges arrayByCompactingContainedDateRanges];
898   [self debugWithFormat:@"  blocked ranges: %@", ranges];
899
900   return [ranges count] != 0 ? YES : NO;
901 }
902
903 /* response generation */
904
905 - (NSString *)jsCode {
906   static NSString *script = \
907   @"function showElement(e, show) {\n"
908   @"    e.style.visibility = show ? 'visible' : 'hidden';\n"
909   @"}\n"
910   @"\n"
911   @"function selectHasCycle(sender) {\n"
912   @"  var value = sender.selectedIndex;\n"
913   @"  var show  = (value != 0);\n"
914   @"  var sel   = document.getElementById('cycle_end_mode_selection');"
915   @"  this.showElement(document.getElementById('cycle_end_label'), show);\n"
916   @"  this.showElement(document.getElementById('cycle_end_mode'),  show);\n"
917   @"  this.selectCycleEnd(sel);\n"
918   @"}\n"
919   @"function selectCycleEnd(sender) {\n"
920   @"  var cycleEndUntil = document.getElementById('cycle_end_until');\n"
921   @"  var value = sender.options[sender.selectedIndex].value;\n"
922   @"  var show  = (value == 'cycle_end_until');\n"
923   @"  this.showElement(cycleEndUntil, show);\n"
924   @"}\n"
925   @"\n";
926
927   return script;
928 }
929
930 - (NSString *)initialCycleVisibility {
931   if (![self hasCycle])
932     return @"visibility: hidden;";
933   return @"visibility: visible;";
934 }
935
936 - (NSString *)initialCycleEndUntilVisibility {
937   if ([self isCycleEndUntil])
938     return @"visibility: visible;";
939   return @"visibility: hidden;";
940 }
941
942
943 /* actions */
944
945 - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{
946   return YES;
947 }
948
949 - (id)testAction {
950   /* for testing only */
951   WORequest       *req;
952   SOGoAppointment *apt;
953   NSString        *content;
954
955   req = [[self context] request];
956   apt = [[SOGoAppointment alloc] initWithICalString:[self iCalString]];
957   [self saveValuesIntoAppointment:apt];
958   content = [apt iCalString];
959   [self logWithFormat:@"%s -- iCal:\n%@",
960     __PRETTY_FUNCTION__,
961     content];
962   [apt release];
963   return self;
964 }
965
966 - (id<WOActionResults>)defaultAction {
967   NSString *ical;
968   
969   /* load iCalendar file */
970   
971   // TODO: can't we use [clientObject contentAsString]?
972   ical = [[self clientObject] valueForKey:@"iCalString"];
973   if ([ical length] == 0) /* a new appointment */
974     ical = [self contentStringTemplate];
975   
976   [self setContentString:ical];
977   [self loadValuesFromContentString:ical];
978   
979   if (![self canEditApt]) {
980     /* TODO: we need proper ACLs */
981     return [self redirectToLocation:[self _completeURIForMethod:@"../view"]];
982   }
983   return self;
984 }
985
986 - (id)saveAction {
987   SOGoAppointment *apt;
988   iCalPerson      *p;
989   NSString        *content;
990   NSException     *ex;
991   
992   if (![self isWriteableClientObject]) {
993     /* return 400 == Bad Request */
994     return [NSException exceptionWithHTTPStatus:400
995                         reason:@"method cannot be invoked on "
996                                @"the specified object"];
997   }
998   
999   apt = [[SOGoAppointment alloc] initWithICalString:[self iCalString]];
1000   if (apt == nil) {
1001     NSString *s;
1002     
1003     s = [self labelForKey:@"Invalid iCal data!"];
1004     [self setErrorText:s];
1005     return self;
1006   }
1007   
1008   [self saveValuesIntoAppointment:apt];
1009   p = [apt findParticipantWithEmail:[self emailForUser]];
1010   if (p) {
1011     [p setParticipationStatus:iCalPersonPartStatAccepted];
1012   }
1013
1014   if ([self checkForConflicts]) {
1015     if ([self containsConflict:apt]) {
1016       NSString *s;
1017       
1018       s = [self labelForKey:@"Conflicts found!"];
1019       [self setErrorText:s];
1020       [apt release];
1021       return self;
1022     }
1023   }
1024   content = [apt iCalString];
1025   [apt release]; apt = nil;
1026   
1027   if (content == nil) {
1028     NSString *s;
1029     
1030     s = [self labelForKey:@"Could not create iCal data!"];
1031     [self setErrorText:s];
1032     return self;
1033   }
1034   
1035   ex = [[self clientObject] saveContentString:content];
1036   if (ex != nil) {
1037     [self setErrorText:[ex reason]];
1038     return self;
1039   }
1040   
1041   return [self redirectToLocation:[self _completeURIForMethod:@".."]];
1042 }
1043
1044 - (id)acceptAction {
1045   return [self acceptOrDeclineAction:YES];
1046 }
1047
1048 - (id)declineAction {
1049   return [self acceptOrDeclineAction:NO];
1050 }
1051
1052 // TODO: add tentatively
1053
1054 - (id)acceptOrDeclineAction:(BOOL)_accept {
1055   // TODO: this should live in the SoObjects
1056   SOGoAppointment *apt;
1057   iCalPerson      *p;
1058   NSString        *iCal, *content;
1059   NSException     *ex;
1060   
1061   if (![self isWriteableClientObject]) {
1062     /* 400 == Bad Request */
1063     return [NSException exceptionWithHTTPStatus:400
1064                         reason:@"method cannot be invoked on "
1065                                @"the specified object"];
1066   }
1067   iCal = [[self clientObject] valueForKey:@"iCalString"];
1068   apt  = [[SOGoAppointment alloc] initWithICalString:iCal];
1069   if (apt == nil) {
1070     /* 500 == Internal Server Error */
1071     return [NSException exceptionWithHTTPStatus:500
1072                         reason:@"unable to parse appointment"];
1073   }
1074   
1075   if ((p = [apt findParticipantWithEmail:[self emailForUser]]) == nil) {
1076     /* 404 == Not found */
1077     return [NSException exceptionWithHTTPStatus:404
1078                         reason:@"user does not participate in this "
1079                                @"appointment"];
1080   }
1081
1082   // TODO: add tentative
1083   if (_accept)
1084     [p setParticipationStatus:iCalPersonPartStatAccepted];
1085   else
1086     [p setParticipationStatus:iCalPersonPartStatDeclined];
1087
1088   content = [apt iCalString];
1089   [apt release];
1090   
1091   if (content == nil) {
1092     /* 500 == Internal Server Error */
1093     return [NSException exceptionWithHTTPStatus:500
1094                         reason:@"Could not create iCalendar data ..."];
1095   }
1096   
1097   ex = [[self clientObject] saveContentString:content];
1098   if (ex != nil) {
1099     /* 500 == Internal Server Error */
1100     return [NSException exceptionWithHTTPStatus:500
1101                         reason:[ex reason]];
1102   }
1103   
1104   return [self redirectToLocation:[self _completeURIForMethod:@"../view"]];
1105 }
1106
1107 @end /* UIxAppointmentEditor */