1/* iCalToDot+SOGo.m - this file is part of SOGo
2 *
3 * Copyright (C) 2008-2017 Inverse inc.
4 *
5 * This file is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2, or (at your option)
8 * any later version.
9 *
10 * This file is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; see the file COPYING.  If not, write to
17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
19 */
20
21#import <Foundation/NSDictionary.h>
22#import <Foundation/NSValue.h>
23
24#import <NGExtensions/NSNull+misc.h>
25#import <NGExtensions/NSObject+Logs.h>
26
27#import <NGCards/NSString+NGCards.h>
28#import <NGCards/iCalCalendar.h>
29#import <NGCards/iCalDateTime.h>
30#import <NGCards/iCalPerson.h>
31#import <NGCards/iCalTimeZone.h>
32
33#import <SoObjects/SOGo/WOContext+SOGo.h>
34
35#import <SOGo/CardElement+SOGo.h>
36#import <SOGo/NSCalendarDate+SOGo.h>
37#import <SOGo/SOGoUser.h>
38#import <SOGo/SOGoUserDefaults.h>
39
40#import "iCalRepeatableEntityObject+SOGo.h"
41
42#import "iCalToDo+SOGo.h"
43
44@implementation iCalToDo (SOGoExtensions)
45
46+ (NSString *) statusForCode: (int) statusCode
47{
48  if (statusCode == taskStatusCompleted)
49    return @"completed";
50  else if (statusCode == taskStatusInProcess)
51    return @"in-process";
52  else if (statusCode == taskStatusCancelled)
53    return @"cancelled";
54  else if (statusCode == taskStatusNeedsAction)
55    return @"needs-action";
56
57  return @"";
58}
59
60- (NSDictionary *) attributesInContext: (WOContext *) context
61{
62  BOOL isAllDayStartDate, isAllDayDueDate;
63  NSCalendarDate *startDate, *dueDate, *completedDate;
64  NSMutableDictionary *data;
65  NSTimeZone *timeZone;
66  SOGoUserDefaults *ud;
67
68  ud = [[context activeUser] userDefaults];
69  timeZone = [ud timeZone];
70
71  startDate = [self startDate];
72  isAllDayStartDate = [(iCalDateTime *) [self uniqueChildWithTag: @"dtstart"] isAllDay];
73  if (!isAllDayStartDate)
74    [startDate setTimeZone: timeZone];
75
76  dueDate = [self due];
77  isAllDayDueDate = [(iCalDateTime *) [self uniqueChildWithTag: @"due"] isAllDay];
78  if (!isAllDayDueDate)
79    [dueDate setTimeZone: timeZone];
80
81  completedDate = [self completed];
82  [completedDate setTimeZone: timeZone];
83
84  data = [NSMutableDictionary dictionaryWithDictionary: [super attributesInContext: context]];
85
86  if (startDate)
87    [data setObject: [startDate iso8601DateString] forKey: @"startDate"];
88  if (dueDate)
89    [data setObject: [dueDate iso8601DateString] forKey: @"dueDate"];
90  if (completedDate)
91    [data setObject: [completedDate iso8601DateString] forKey: @"completedDate"];
92
93  if ([[self percentComplete] length])
94    [data setObject: [NSNumber numberWithInt: [[self percentComplete] intValue]] forKey: @"percentComplete"];
95
96  return data;
97}
98
99/**
100 * @see [iCalRepeatableEntityObject+SOGo setAttributes:inContext:]
101 * @see [iCalEntityObject+SOGo setAttributes:inContext:]
102 * @see [UIxAppointmentEditor saveAction]
103 */
104- (void) setAttributes: (NSDictionary *) data
105             inContext: (WOContext *) context
106{
107  BOOL isAllDayStartDate, isAllDayDueDate;
108  NSCalendarDate *startDate, *dueDate, *completedDate;
109  NSInteger percent;
110  SOGoUserDefaults *ud;
111  iCalDateTime *todoStartDate, *todoDueDate;
112  iCalTimeZone *tz;
113  id o;
114
115  [super setAttributes: data inContext: context];
116
117  startDate = dueDate = completedDate = nil;
118
119  // Handle start date
120  isAllDayStartDate = YES;
121  o = [data objectForKey: @"startDate"];
122  if ([o isKindOfClass: [NSString class]] && [o length])
123    startDate = [self dateFromString: o inContext: context];
124
125  o = [data objectForKey: @"startTime"];
126  if ([o isKindOfClass: [NSString class]] && [o length])
127    {
128      isAllDayStartDate = NO;
129      [self adjustDate: &startDate withTimeString: o inContext: context];
130    }
131
132  if (startDate)
133    [self setStartDate: startDate];
134  else
135    [self setStartDate: nil];
136
137  // Handle due date
138  isAllDayDueDate = YES;
139  o = [data objectForKey: @"dueDate"];
140  if ([o isKindOfClass: [NSString class]] && [o length])
141    dueDate = [self dateFromString: o inContext: context];
142
143  o = [data objectForKey: @"dueTime"];
144  if ([o isKindOfClass: [NSString class]] && [o length])
145    {
146      isAllDayDueDate = NO;
147      [self adjustDate: &dueDate withTimeString: o inContext: context];
148    }
149
150  if (dueDate)
151    if (isAllDayDueDate)
152      [self setAllDayDue: dueDate];
153    else
154      [self setDue: dueDate];
155  else
156    [self setDue: nil];
157
158  if (!startDate && !dueDate)
159    [self removeAllAlarms];
160
161  // Handle time zone
162  todoStartDate = (iCalDateTime *)[self uniqueChildWithTag: @"dtstart"];
163  todoDueDate = (iCalDateTime *)[self uniqueChildWithTag: @"due"];
164  tz = [todoStartDate timeZone];
165  if (!tz)
166    tz = [todoDueDate timeZone];
167
168  if (isAllDayStartDate && isAllDayDueDate)
169    {
170      if (tz)
171        [[self parent] removeChild: tz];
172      [todoStartDate setTimeZone: nil];
173      [todoDueDate setTimeZone: nil];
174    }
175  else
176    {
177      if (!tz)
178        {
179          ud = [[context activeUser] userDefaults];
180          tz = [iCalTimeZone timeZoneForName: [ud timeZoneName]];
181        }
182      if (tz)
183        {
184          [[self parent] addTimeZone: tz];
185          if (todoStartDate)
186            {
187              if (isAllDayStartDate)
188                [todoStartDate setTimeZone: nil];
189              else if (![todoStartDate timeZone])
190                [todoStartDate setTimeZone: tz];
191            }
192          if (todoDueDate)
193            {
194              if (isAllDayDueDate)
195                [todoDueDate setTimeZone: nil];
196              else if (![todoDueDate timeZone])
197                [todoDueDate setTimeZone: tz];
198            }
199        }
200    }
201
202  // Handle completed date
203  o = [data objectForKey: @"completedDate"];
204  if ([o isKindOfClass: [NSString class]] && [o length])
205    {
206      completedDate = [self dateFromString: o inContext: context];
207
208      o = [data objectForKey: @"completedTime"];
209      if ([o isKindOfClass: [NSString class]] && [o length])
210        [self adjustDate: &completedDate withTimeString: o inContext: context];
211    }
212  else
213    [(iCalDateTime *) [self uniqueChildWithTag: @"completed"] setDateTime: nil];
214
215  o = [self status];
216  if ([o length])
217    {
218      if ([o isEqualToString: @"COMPLETED"] && completedDate)
219        // As specified in RFC5545, the COMPLETED property must use UTC time
220        [self setCompleted: completedDate];
221    }
222
223  // Percent complete
224  o = [data objectForKey: @"percentComplete"];
225  if ([o isKindOfClass: [NSNumber class]])
226    {
227      percent = [o intValue];
228      if (percent >= 0 && percent <= 100)
229        [self setPercentComplete: [NSString stringWithFormat: @"%i", (int)percent]];
230    }
231  else
232    [self setPercentComplete: @""];
233}
234
235- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent
236                                       container: (id) theContainer
237                                 nameInContainer: (NSString *) nameInContainer
238{
239  NSMutableDictionary *row;
240  NSCalendarDate *startDate, *dueDate, *completed;
241  NSArray *attendees, *categories;
242  NSString *uid, *title, *location, *status;
243  NSNumber *sequence;
244  id organizer, date;
245  id participants, partmails;
246  NSMutableString *partstates;
247  unsigned i, count, code;
248  iCalAccessClass accessClass;
249
250  /* extract values */
251
252  startDate = [self startDate];
253  dueDate = [self due];
254  uid = [self uid];
255  title = [self summary];
256  if (![title isNotNull])
257    title = @"";
258  location = [self location];
259  sequence = [self sequence];
260  accessClass = [self symbolicAccessClass];
261  completed = [self completed];
262  status = [[self status] uppercaseString];
263
264  attendees = [self attendees];
265  partmails = [attendees valueForKey: @"rfc822Email"];
266  partmails = [partmails componentsJoinedByString: @"\n"];
267  participants = [attendees valueForKey: @"cn"];
268  participants = [participants componentsJoinedByString: @"\n"];
269
270  /* build row */
271
272  row = [NSMutableDictionary dictionaryWithCapacity:8];
273
274  [row setObject: @"vtodo" forKey: @"c_component"];
275
276  if ([uid isNotNull])
277    [row setObject:uid forKey: @"c_uid"];
278  else
279    [self logWithFormat: @"WARNING: could not extract a uid from event!"];
280
281  [row setObject:[NSNumber numberWithBool:[self isRecurrent]]
282       forKey: @"c_iscycle"];
283  [row setObject:[NSNumber numberWithInt:[self priorityNumber]]
284       forKey: @"c_priority"];
285
286  [row setObject: [NSNumber numberWithBool: NO]
287       forKey: @"c_isallday"];
288  [row setObject: [NSNumber numberWithBool: NO]
289       forKey: @"c_isopaque"];
290
291  [row setObject: title forKey: @"c_title"];
292  if ([location isNotNull]) [row setObject: location forKey: @"c_location"];
293  if ([sequence isNotNull]) [row setObject: sequence forKey: @"c_sequence"];
294
295  if ([startDate isNotNull])
296    date = [self quickRecordDateAsNumber: startDate
297                              withOffset: 0 forAllDay: NO];
298  else
299    date = [NSNull null];
300  [row setObject: date forKey: @"c_startdate"];
301
302  if ([dueDate isNotNull])
303    date = [self quickRecordDateAsNumber: dueDate
304                              withOffset: 0 forAllDay: NO];
305  else
306    date = [NSNull null];
307  [row setObject: date forKey: @"c_enddate"];
308
309  if ([self isRecurrent])
310    {
311      [row setObject: iCalDistantFutureNumber forKey: @"c_cycleenddate"];
312      [row setObject: [self cycleInfo] forKey: @"c_cycleinfo"];
313    }
314
315  if ([participants length] > 0)
316    [row setObject:participants forKey: @"c_participants"];
317  if ([partmails length] > 0)
318    [row setObject:partmails forKey: @"c_partmails"];
319
320  if (completed || [status isNotNull])
321    {
322      code = 0;
323      if (completed || [status isEqualToString: @"COMPLETED"])
324        code = taskStatusCompleted;
325      else if ([status isEqualToString: @"IN-PROCESS"])
326        code = taskStatusInProcess;
327      else if ([status isEqualToString: @"CANCELLED"])
328        code = taskStatusCancelled;
329      else if ([status isEqualToString: @"NEEDS-ACTION"])
330        code = taskStatusNeedsAction;
331      [row setObject: [NSNumber numberWithInt: code] forKey: @"c_status"];
332    }
333  else
334    {
335      /* confirmed by default */
336      [row setObject: [NSNumber numberWithInt: 0] forKey: @"c_status"];
337    }
338
339  [row setObject: [NSNumber numberWithUnsignedInt: accessClass]
340       forKey: @"c_classification"];
341
342  organizer = [self organizer];
343  if (organizer)
344    {
345      NSString *email;
346
347      email = [organizer valueForKey: @"rfc822Email"];
348      if (email)
349        [row setObject:email forKey: @"c_orgmail"];
350    }
351
352  /* construct partstates */
353  count = [attendees count];
354  partstates = [[NSMutableString alloc] initWithCapacity:count * 2];
355  for (i = 0; i < count; i++)
356    {
357      iCalPerson *p;
358      iCalPersonPartStat stat;
359
360      p = [attendees objectAtIndex:i];
361      stat = [p participationStatus];
362      if(i != 0)
363        [partstates appendString: @"\n"];
364      [partstates appendFormat: @"%d", stat];
365    }
366  [row setObject:partstates forKey: @"c_partstates"];
367  [partstates release];
368
369  /* handle alarms */
370  [self updateNextAlarmDateInRow: row  forContainer: theContainer  nameInContainer: nameInContainer];
371
372  categories = [self categories];
373  if ([categories count] > 0)
374    [row setObject: [categories componentsJoinedByString: @","]
375            forKey: @"c_category"];
376
377  /* handle description */
378  if ([self comment])
379    [row setObject: [self comment]  forKey: @"c_description"];
380  else
381    [row setObject: [NSNull null] forKey: @"c_description"];
382
383  return row;
384}
385
386- (NSTimeInterval) occurenceInterval
387{
388  if ([self due])
389    return [[self due] timeIntervalSinceDate: [self startDate]];
390  else
391    // When no due date is defined, base recurrence calculation on a 60-minute duration
392    return 3600;
393}
394
395@end
396