]> err.no Git - scalable-opengroupware.org/blob - UI/Scheduler/UIxAppointmentEditor.m
5c2d39c72de757a49824a40209b891d3aac3197e
[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 #import <NGCards/NSString+NGCards.h>
23 #import <NGCards/NSCalendarDate+NGCards.h>
24
25 #import <SOGo/AgenorUserManager.h>
26 #import <SOGo/NSCalendarDate+SOGo.h>
27
28 #import "common.h"
29 #import <NGCards/NGCards.h>
30 #import <NGExtensions/NGCalendarDateRange.h>
31 #import <SOGoUI/SOGoDateFormatter.h>
32 #import <Appointments/SOGoAppointmentFolder.h>
33 #import <Appointments/SOGoAppointmentObject.h>
34 #import "UIxComponent+Agenor.h"
35
36 #import "UIxAppointmentEditor.h"
37
38 /* TODO: CLEAN UP */
39
40 @implementation UIxAppointmentEditor
41
42 + (int)version {
43   return [super version] + 0 /* v2 */;
44 }
45
46 + (void)initialize {
47   NSAssert2([super version] == 2,
48             @"invalid superclass (%@) version %i !",
49             NSStringFromClass([self superclass]), [super version]);
50 }
51
52 - (void) dealloc
53 {
54   [endDate release];
55   [super dealloc];
56 }
57
58 /* accessors */
59
60 - (void) setAptStartDate: (NSCalendarDate *)_date
61 {
62   [self setStartDate: _date];
63 }
64
65 - (NSCalendarDate *) aptStartDate
66 {
67   return [self startDate];
68 }
69
70 - (void) setAptEndDate: (NSCalendarDate *) _date
71 {
72   ASSIGN(endDate, _date);
73 }
74
75 - (NSCalendarDate *) aptEndDate
76 {
77   return endDate;
78 }
79
80 /* transparency */
81
82 - (NSString *) transparency
83 {
84   return @"OPAQUE";
85 }
86
87 /* iCal */
88
89 - (NSString *)iCalStringTemplate {
90   static NSString *iCalStringTemplate = \
91     @"BEGIN:VCALENDAR\r\n"
92     @"METHOD:REQUEST\r\n"
93     @"PRODID://Inverse groupe conseil/SOGo 0.9\r\n"
94     @"VERSION:2.0\r\n"
95     @"BEGIN:VEVENT\r\n"
96     @"UID:%@\r\n"
97     @"CLASS:PUBLIC\r\n"
98     @"STATUS:CONFIRMED\r\n" /* confirmed by default */
99     @"DTSTAMP:%@Z\r\n"
100     @"DTSTART:%@Z\r\n"
101     @"DTEND:%@Z\r\n"
102     @"TRANSP:%@\r\n"
103     @"SEQUENCE:1\r\n"
104     @"PRIORITY:5\r\n"
105     @"%@"                   /* organizer */
106     @"%@"                   /* participants and resources */
107     @"END:VEVENT\r\n"
108     @"END:VCALENDAR";
109
110   NSTimeZone *utc;
111   NSCalendarDate *lStartDate, *lEndDate, *stamp;
112   NSString *template, *s;
113   unsigned minutes;
114
115   s = [self queryParameterForKey:@"dur"];
116   if ([s length] > 0)
117     minutes = [s intValue];
118   else
119     minutes = 60;
120
121   utc = [NSTimeZone timeZoneWithName: @"GMT"];
122   lStartDate = [self newStartDate];
123   [lStartDate setTimeZone: utc];
124   lEndDate = [lStartDate dateByAddingYears: 0 months: 0 days: 0
125                          hours: 0 minutes: minutes seconds: 0];
126
127   stamp = [NSCalendarDate calendarDate];
128   [stamp setTimeZone: utc];
129
130   s          = [self iCalParticipantsAndResourcesStringFromQueryParameters];
131   template   = [NSString stringWithFormat:iCalStringTemplate,
132                          [[self clientObject] nameInContainer],
133                          [stamp iCalFormattedDateTimeString],
134                          [lStartDate iCalFormattedDateTimeString],
135                          [lEndDate iCalFormattedDateTimeString],
136                          [self transparency],
137                          [self iCalOrganizerString],
138                          s];
139   return template;
140 }
141
142 /* new */
143
144 - (id) newAction
145 {
146   /*
147     This method creates a unique ID and redirects to the "edit" method on the
148     new ID.
149     It is actually a folder method and should be defined on the folder.
150     
151     Note: 'clientObject' is the SOGoAppointmentFolder!
152           Update: remember that there are group folders as well.
153   */
154   NSString *uri, *objectId, *method, *ps;
155
156   objectId = [NSClassFromString(@"SOGoAppointmentFolder")
157                                globallyUniqueObjectId];
158   if ([objectId length] == 0) {
159     return [NSException exceptionWithHTTPStatus:500 /* Internal Error */
160                         reason:@"could not create a unique ID"];
161   }
162
163   method = [NSString stringWithFormat:@"Calendar/%@/editAsAppointment", objectId];
164   method = [[self userFolderPath] stringByAppendingPathComponent:method];
165
166   /* check if participants have already been provided */
167   ps     = [self queryParameterForKey:@"ps"];
168 //   if (ps) {
169 //     [self setQueryParameter:ps forKey:@"ps"];
170 //   }
171  if (!ps
172      && [[self clientObject] respondsToSelector:@selector(calendarUIDs)]) {
173     AgenorUserManager *um;
174     NSArray *uids;
175     NSMutableArray *emails;
176     unsigned i, count;
177
178     /* add all current calendarUIDs as default participants */
179
180     um     = [AgenorUserManager sharedUserManager];
181     uids   = [[self clientObject] calendarUIDs];
182     count  = [uids count];
183     emails = [NSMutableArray arrayWithCapacity:count];
184     
185     for (i = 0; i < count; i++) {
186       NSString *email;
187       
188       email = [um getEmailForUID:[uids objectAtIndex:i]];
189       if (email)
190         [emails addObject:email];
191     }
192     ps = [emails componentsJoinedByString:@","];
193     [self setQueryParameter:ps forKey:@"ps"];
194   }
195   uri = [self completeHrefForMethod:method];
196   return [self redirectToLocation:uri];
197 }
198
199 /* save */
200
201 - (void) loadValuesFromAppointment: (iCalEvent *) appointment
202 {
203   NSTimeZone *uTZ;
204
205   [self loadValuesFromComponent: appointment];
206
207   uTZ = [[self clientObject] userTimeZone];
208   endDate = [appointment endDate];
209   if (!endDate)
210     endDate = [[self startDate] dateByAddingYears: 0 months: 0 days: 0
211                                 hours: 1 minutes: 0 seconds: 0];
212
213   [endDate setTimeZone: uTZ];
214   [endDate retain];
215 }
216
217 - (void) saveValuesIntoAppointment: (iCalEvent *) _appointment
218 {
219   /* merge in form values */
220   NSArray *attendees, *lResources;
221   iCalRecurrenceRule *rrule;
222   
223   [_appointment setStartDate:[self aptStartDate]];
224   [_appointment setEndDate:[self aptEndDate]];
225
226   [_appointment setSummary: [self title]];
227   [_appointment setUrl: [self url]];
228   [_appointment setLocation: [self location]];
229   [_appointment setComment: [self comment]];
230   [_appointment setPriority:[self priority]];
231   [_appointment setAccessClass: [self privacy]];
232   [_appointment setStatus: [self status]];
233
234 //   [_appointment setCategories: [[self categories] componentsJoinedByString: @","]];
235
236   [_appointment setTransparency: [self transparency]];
237
238 #if 0
239   /*
240     Note: bad, bad, bad!
241     Organizer is no form value, thus we MUST NOT change it
242   */
243   [_appointment setOrganizer:organizer];
244 #endif
245   attendees  = [self participants];
246   lResources = [self resources];
247   if ([lResources count] > 0) {
248     attendees = ([attendees count] > 0)
249       ? [attendees arrayByAddingObjectsFromArray: lResources]
250       : lResources;
251   }
252   [attendees makeObjectsPerformSelector: @selector (setTag:)
253              withObject: @"attendee"];
254   [_appointment setAttendees: attendees];
255
256   /* cycles */
257   [_appointment removeAllRecurrenceRules];
258   rrule = [self rrule];
259   if (rrule)
260     [_appointment addToRecurrenceRules: rrule];
261 }
262
263 - (iCalEvent *) appointmentFromString: (NSString *) _iCalString
264 {
265   iCalCalendar *calendar;
266   iCalEvent *appointment;
267
268   calendar = [iCalCalendar parseSingleFromSource: _iCalString];
269   appointment = (iCalEvent *) [calendar firstChildWithTag: @"vevent"];
270
271   return appointment;
272 }
273
274 /* conflict management */
275
276 - (BOOL) containsConflict: (id) _apt
277 {
278   NSArray *attendees, *uids;
279   SOGoAppointmentFolder *groupCalendar;
280   NSArray *infos;
281   NSArray *ranges;
282   id folder;
283
284   [self logWithFormat:@"search from %@ to %@", 
285           [_apt startDate], [_apt endDate]];
286
287   folder    = [[self clientObject] container];
288   attendees = [_apt attendees];
289   uids      = [folder uidsFromICalPersons:attendees];
290   if ([uids count] == 0) {
291     [self logWithFormat:@"Note: no UIDs selected."];
292     return NO;
293   }
294
295   groupCalendar = [folder lookupGroupCalendarFolderForUIDs:uids
296                           inContext:[self context]];
297   [self debugWithFormat:@"group calendar: %@", groupCalendar];
298   
299   if (![groupCalendar respondsToSelector:@selector(fetchFreeBusyInfosFrom:to:)]) {
300     [self errorWithFormat:@"invalid folder to run freebusy query on!"];
301     return NO;
302   }
303
304   infos = [groupCalendar fetchFreeBusyInfosFrom:[_apt startDate]
305                          to:[_apt endDate]];
306   [self debugWithFormat:@"  process: %d events", [infos count]];
307
308   ranges = [infos arrayByCreatingDateRangesFromObjectsWithStartDateKey: @"startDate"
309                   andEndDateKey: @"endDate"];
310   ranges = [ranges arrayByCompactingContainedDateRanges];
311   [self debugWithFormat:@"  blocked ranges: %@", ranges];
312
313   return [ranges count] != 0 ? YES : NO;
314 }
315
316 /* actions */
317
318 - (id) testAction
319 {
320   /* for testing only */
321   WORequest *req;
322   iCalEvent *apt;
323   NSString *content;
324
325   req = [[self context] request];
326   apt = [self appointmentFromString: [self iCalString]];
327   [self saveValuesIntoAppointment:apt];
328   content = [[apt parent] versitString];
329   [self logWithFormat:@"%s -- iCal:\n%@",
330     __PRETTY_FUNCTION__,
331     content];
332
333   return self;
334 }
335
336 - (id<WOActionResults>) defaultAction
337 {
338   NSString *ical;
339   
340   /* load iCalendar file */
341   
342   // TODO: can't we use [clientObject contentAsString]?
343 //   ical = [[self clientObject] valueForKey:@"iCalString"];
344   ical = [[self clientObject] contentAsString];
345   if ([ical length] == 0) /* a new appointment */
346     ical = [self iCalStringTemplate];
347
348   [self setICalString:ical];
349   [self loadValuesFromAppointment: [self appointmentFromString: ical]];
350
351 //   if (![self canEditComponent]) {
352 //     /* TODO: we need proper ACLs */
353 //     return [self redirectToLocation: [self completeURIForMethod: @"../view"]];
354 //   }
355   return self;
356 }
357
358 - (id <WOActionResults>) saveAction
359 {
360   iCalEvent *apt;
361   iCalPerson *p;
362   id <WOActionResults> result;
363   NSString *content;
364   NSException *ex;
365
366   if (![self isWriteableClientObject]) {
367     /* return 400 == Bad Request */
368     return [NSException exceptionWithHTTPStatus:400
369                         reason:@"method cannot be invoked on "
370                                @"the specified object"];
371   }
372   
373   apt = [self appointmentFromString: [self iCalString]];
374   if (apt == nil) {
375     NSString *s;
376     
377     s = [self labelForKey:@"Invalid iCal data!"];
378     [self setErrorText:s];
379     return self;
380   }
381   
382   [self saveValuesIntoAppointment:apt];
383   p = [apt findParticipantWithEmail:[self emailForUser]];
384   if (p) {
385     [p setParticipationStatus:iCalPersonPartStatAccepted];
386   }
387
388   if ([self checkForConflicts]) {
389     if ([self containsConflict:apt]) {
390       NSString *s;
391       
392       s = [self labelForKey:@"Conflicts found!"];
393       [self setErrorText:s];
394
395       return self;
396     }
397   }
398   content = [[apt parent] versitString];
399 //   [apt release]; apt = nil;
400   
401   if (content == nil) {
402     NSString *s;
403     
404     s = [self labelForKey:@"Could not create iCal data!"];
405     [self setErrorText:s];
406     return self;
407   }
408   
409   ex = [[self clientObject] saveContentString:content];
410   if (ex != nil) {
411     [self setErrorText:[ex reason]];
412     return self;
413   }
414   
415   if ([[[[self context] request] formValueForKey: @"nojs"] intValue])
416     result = [self redirectToLocation: [self applicationPath]];
417   else
418     result = [self jsCloseWithRefreshMethod: @"refreshAppointmentsAndDisplay()"];
419
420   return result;
421 }
422
423 - (NSString *) saveUrl
424 {
425   return [NSString stringWithFormat: @"%@/saveAsAppointment",
426                    [[self clientObject] baseURL]];
427 }
428
429 - (id) acceptAction
430 {
431   return [self acceptOrDeclineAction:YES];
432 }
433
434 - (id) declineAction
435 {
436   return [self acceptOrDeclineAction:NO];
437 }
438
439 // TODO: add tentatively
440
441 - (id) acceptOrDeclineAction: (BOOL) _accept
442 {
443   // TODO: this should live in the SoObjects
444   NSException *ex;
445
446   if ((ex = [self validateObjectForStatusChange]) != nil)
447     return ex;
448   
449   ex = [[self clientObject] changeParticipationStatus:
450                               _accept ? @"ACCEPTED" : @"DECLINED"
451                             inContext:[self context]];
452   if (ex != nil) return ex;
453
454   return self;  
455 //   return [self redirectToLocation: [self completeURIForMethod: @"../view"]];
456 }
457
458 @end /* UIxAppointmentEditor */