diff --git a/SOPE/GDLContentStore/GCSAlarmsFolder.m b/SOPE/GDLContentStore/GCSAlarmsFolder.m index 8ab411b20..2140ba7da 100644 --- a/SOPE/GDLContentStore/GCSAlarmsFolder.m +++ b/SOPE/GDLContentStore/GCSAlarmsFolder.m @@ -1,8 +1,6 @@ /* GCSAlarmsFolder.m - this file is part of SOGo * - * Copyright (C) 2010 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2010-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SOPE/GDLContentStore/GCSFolder.m b/SOPE/GDLContentStore/GCSFolder.m index 2b05ef67e..5823f239a 100644 --- a/SOPE/GDLContentStore/GCSFolder.m +++ b/SOPE/GDLContentStore/GCSFolder.m @@ -944,7 +944,7 @@ andAttribute: (EOAttribute *)_attribute || *_baseVersion == [storedVersion unsignedIntValue]) { /* extract quick info */ - quickRow = [theComponent performSelector: @selector(quickRecordForContainer:) withObject: theContainer]; + quickRow = [theComponent performSelector: @selector(quickRecordFromContent:container:) withObject: _content withObject: theContainer]; if (quickRow) { [quickRow setObject:_name forKey:@"c_name"]; diff --git a/SOPE/NGCards/CardElement.h b/SOPE/NGCards/CardElement.h index 21e6c4d1d..92ad9a61b 100644 --- a/SOPE/NGCards/CardElement.h +++ b/SOPE/NGCards/CardElement.h @@ -1,15 +1,13 @@ /* CardElement.h - this file is part of SOPE * - * Copyright (C) 2006-2012 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2006-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * - * This file is distributed in the hope that it will be useful, + * This file is distributed in the hope that it will be useful,] * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. diff --git a/SOPE/NGCards/CardElement.m b/SOPE/NGCards/CardElement.m index 834caf671..00d7c7b35 100644 --- a/SOPE/NGCards/CardElement.m +++ b/SOPE/NGCards/CardElement.m @@ -1,8 +1,6 @@ /* CardElement.m - this file is part of SOPE * - * Copyright (C) 2006-2012 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2006-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SOPE/NGCards/iCalDateTime.m b/SOPE/NGCards/iCalDateTime.m index 74e89228e..1cfa782d5 100644 --- a/SOPE/NGCards/iCalDateTime.m +++ b/SOPE/NGCards/iCalDateTime.m @@ -1,8 +1,6 @@ /* iCalDateTime.m - this file is part of SOPE * - * Copyright (C) 2006-2011 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2006-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -92,8 +90,8 @@ calendar = (iCalCalendar *) [self searchParentOfClass: [iCalCalendar class]]; timeZone = [calendar timeZoneWithId: tzId]; - if (!timeZone) - [self logWithFormat: @"timezone '%@' not found in calendar", tzId]; + //if (!timeZone) + //[self logWithFormat: @"timezone '%@' not found in calendar", tzId]; } return timeZone; diff --git a/SOPE/NGCards/iCalEvent.h b/SOPE/NGCards/iCalEvent.h index 05fe05c5b..f43a72924 100644 --- a/SOPE/NGCards/iCalEvent.h +++ b/SOPE/NGCards/iCalEvent.h @@ -66,8 +66,6 @@ - (BOOL) isWithinCalendarDateRange: (NGCalendarDateRange *) _range; - (NSArray *) recurrenceRangesWithinCalendarDateRange: (NGCalendarDateRange *)_r; -- (NSCalendarDate *) lastPossibleRecurrenceStartDate; - /* calculating changes */ - (iCalEventChanges *) getChangesRelativeToEvent: (iCalEvent *) _event; diff --git a/SOPE/NGCards/iCalRepeatableEntityObject.h b/SOPE/NGCards/iCalRepeatableEntityObject.h index 4f7570b59..52b2d3a93 100644 --- a/SOPE/NGCards/iCalRepeatableEntityObject.h +++ b/SOPE/NGCards/iCalRepeatableEntityObject.h @@ -69,6 +69,8 @@ - (NSCalendarDate *) firstRecurrenceStartDateWithEndDate: (NSCalendarDate *) endDate; +- (NSCalendarDate *) lastPossibleRecurrenceStartDate; + @end #endif /* __NGCards_iCalRepeatableEntityObject_H_ */ diff --git a/SOPE/NGCards/iCalRepeatableEntityObject.m b/SOPE/NGCards/iCalRepeatableEntityObject.m index 5a7dee12c..4f201307f 100644 --- a/SOPE/NGCards/iCalRepeatableEntityObject.m +++ b/SOPE/NGCards/iCalRepeatableEntityObject.m @@ -417,4 +417,11 @@ lastPossibleRecurrenceStartDateUsingFirstInstanceCalendarDateRange: (NGCalendarD return firstOccurrenceStartDate; } +- (NSCalendarDate *) lastPossibleRecurrenceStartDate +{ + [self subclassResponsibility: _cmd]; + + return nil; +} + @end diff --git a/SOPE/NGCards/iCalToDo.m b/SOPE/NGCards/iCalToDo.m index 6d5bc7837..439bbfe18 100644 --- a/SOPE/NGCards/iCalToDo.m +++ b/SOPE/NGCards/iCalToDo.m @@ -26,6 +26,8 @@ #import "iCalDateTime.h" #import "iCalToDo.h" +#import + @implementation iCalToDo - (Class) classForTag: (NSString *) classTag @@ -119,4 +121,17 @@ // return ms; // } +- (NSCalendarDate *) lastPossibleRecurrenceStartDate +{ + NGCalendarDateRange *fir; + + if (![self isRecurrent]) + return nil; + + fir = [NGCalendarDateRange calendarDateRangeWithStartDate: [self startDate] + endDate: [self due]]; + + return [self lastPossibleRecurrenceStartDateUsingFirstInstanceCalendarDateRange: fir]; +} + @end /* iCalToDo */ diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.h b/SoObjects/Appointments/SOGoAppointmentFolder.h index 05e760909..d535806b4 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.h +++ b/SoObjects/Appointments/SOGoAppointmentFolder.h @@ -1,6 +1,6 @@ /* Copyright (C) 2004-2005 SKYRIX Software AG - Copyright (C) 2007-2012 Inverse inc. + Copyright (C) 2007-2014 Inverse inc. This file is part of SOGo. @@ -50,6 +50,7 @@ @class GCSFolder; @class iCalCalendar; @class iCalTimeZone; +@class NGCalendarDateRange; @class SOGoWebDAVValue; typedef enum { @@ -118,6 +119,11 @@ typedef enum { - (NSArray *) fetchAlarmInfosFrom: (NSNumber *) _startUTCDate to: (NSNumber *) _endUTCDate; +- (void) flattenCycleRecord: (NSDictionary *) theRecord + forRange: (NGCalendarDateRange *) theRange + intoArray: (NSMutableArray *) theRecords + withCalendar: (iCalCalendar *) calendar; + /* URL generation */ - (NSString *) baseURLForAptWithUID: (NSString *) _uid @@ -138,11 +144,6 @@ typedef enum { - (NSArray *) lookupCalendarFoldersForICalPerson: (NSArray *) _persons inContext: (id) _ctx; -// - (id) lookupGroupFolderForUIDs: (NSArray *) _uids -// inContext: (id) _ctx; -// - (id) lookupGroupCalendarFolderForUIDs: (NSArray *) _uids -// inContext: (id) _ctx; - /* bulk fetches */ - (NSString *) aclSQLListingFilter; @@ -186,6 +187,11 @@ typedef enum { - (NSArray *) aclUsersWithProxyWriteAccess: (BOOL) write; +- (void) findEntityForClosestAlarm: (id *) theEntity + timezone: (NSTimeZone *) theTimeZone + startDate: (NSCalendarDate **) theStartDate + endDate: (NSCalendarDate **) theEndDate; + @end #endif /* __Appointments_SOGoAppointmentFolder_H__ */ diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.m b/SoObjects/Appointments/SOGoAppointmentFolder.m index 6845a3d23..19bc54630 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.m +++ b/SoObjects/Appointments/SOGoAppointmentFolder.m @@ -40,6 +40,7 @@ #import #import #import +#import #import #import #import @@ -49,6 +50,7 @@ #import #import #import +#import #import #import #import @@ -74,6 +76,7 @@ #import #import +#import "iCalCalendar+SOGo.h" #import "iCalRepeatableEntityObject+SOGo.h" #import "iCalEvent+SOGo.h" #import "iCalPerson+SOGo.h" @@ -99,7 +102,7 @@ @implementation SOGoAppointmentFolder static NSNumber *sharedYes = nil; -static iCalEvent *iCalEventK = nil; +static Class iCalEventK = nil; + (void) initialize { @@ -769,7 +772,7 @@ static iCalEvent *iCalEventK = nil; * @param theRecord a dictionnary with the attributes of the event. * @return a copy of theRecord with adjusted dates. */ -- (NSMutableDictionary *) fixupRecord: (NSDictionary *) theRecord +- (NSMutableDictionary *) _fixupRecord: (NSDictionary *) theRecord { NSMutableDictionary *record; static NSString *fields[] = { @"c_startdate", @"startDate", @@ -813,12 +816,12 @@ static iCalEvent *iCalEventK = nil; // // // -- (NSArray *) fixupRecords: (NSArray *) theRecords +- (NSArray *) _fixupRecords: (NSArray *) theRecords { // TODO: is the result supposed to be sorted by date? NSMutableArray *ma; unsigned count, max; - id row; // TODO: what is the type of the record? + id row; if (theRecords) { @@ -826,7 +829,7 @@ static iCalEvent *iCalEventK = nil; ma = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { - row = [self fixupRecord: [theRecords objectAtIndex: count]]; + row = [self _fixupRecord: [theRecords objectAtIndex: count]]; if (row) [ma addObject: row]; } @@ -900,6 +903,9 @@ static iCalEvent *iCalEventK = nil; return record; } +// +// +// - (int) _indexOfRecordMatchingDate: (NSCalendarDate *) matchDate inArray: (NSArray *) recordArray { @@ -922,6 +928,9 @@ static iCalEvent *iCalEventK = nil; return recordIndex; } +// +// +// - (void) _fixExceptionRecord: (NSMutableDictionary *) recRecord fromRow: (NSDictionary *) row { @@ -938,12 +947,57 @@ static iCalEvent *iCalEventK = nil; [recRecord setObjects: objects forKeys: fields]; } +// +// +// +- (void) _computeAlarmForRow: (NSMutableDictionary *) row + master: (iCalEntityObject *) master +{ + iCalEntityObject *component; + iCalAlarm *alarm; + + if (![master recurrenceId]) + { + component = [master copy]; + + [component setStartDate: [NSCalendarDate dateWithTimeIntervalSince1970: [[row objectForKey: @"c_startdate"] intValue]]]; + + if ([component isKindOfClass: [iCalEvent class]]) + { + [(iCalEvent *)component setEndDate: [NSCalendarDate dateWithTimeIntervalSince1970: [[row objectForKey: @"c_enddate"] intValue]]]; + } + else + { + [(iCalToDo *)component setDue: [NSCalendarDate dateWithTimeIntervalSince1970: [[row objectForKey: @"c_enddate"] intValue]]]; + } + } + else + { + component = master; + RETAIN(component); + } + + // Check if we have any alarm, that could happen for recurrence exceptions with no + // alarm defined. + if ([[component alarms] count]) + { + alarm = [[component alarms] objectAtIndex: 0]; + [row setObject: [NSNumber numberWithInt: [[alarm nextAlarmDate] timeIntervalSince1970]] + forKey: @"c_nextalarm"]; + } + + RELEASE(component); +} + +// +// +// - (void) _appendCycleException: (iCalRepeatableEntityObject *) component firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir fromRow: (NSDictionary *) row forRange: (NGCalendarDateRange *) dateRange - withTimeZone: (NSTimeZone *) tz - toArray: (NSMutableArray *) ma + withTimeZone: (NSTimeZone *) tz + toArray: (NSMutableArray *) ma { NSCalendarDate *recurrenceId; NSMutableDictionary *newRecord; @@ -983,7 +1037,8 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir { if ([dateRange containsDate: [component startDate]]) { - newRecord = [self fixupRecord: [component quickRecordForContainer: self]]; + // We must pass nill to :container here in order to avoid re-entrancy issues. + newRecord = [self _fixupRecord: [component quickRecordFromContent: nil container: nil]]; [ma replaceObjectAtIndex: recordIndex withObject: newRecord]; } else @@ -998,8 +1053,9 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir else { // The recurrence id of the exception is outside the date range; - // simply add the exception to the records array - newRecord = [self fixupRecord: [component quickRecordForContainer: self]]; + // simply add the exception to the records array. + // We must pass nill to :container here in order to avoid re-entrancy issues. + newRecord = [self _fixupRecord: [component quickRecordFromContent: nil container: nil]]; newRecordRange = [NGCalendarDateRange calendarDateRangeWithStartDate: [newRecord objectForKey: @"startDate"] endDate: [newRecord objectForKey: @"endDate"]]; @@ -1024,6 +1080,10 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir [self _fixExceptionRecord: newRecord fromRow: row]; } + + // We finally adjust the c_nextalarm + [self _computeAlarmForRow: (id)row + master: component]; } // @@ -1033,30 +1093,32 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir forRange: (NGCalendarDateRange *) dateRange withTimeZone: (NSTimeZone *) tz + withCalendar: (iCalCalendar *) calendar toArray: (NSMutableArray *) ma { - NSArray *elements, *components; - unsigned int count, max; + NSArray *components; NSString *content; + unsigned int count, max; + content = [row objectForKey: @"c_content"]; - if ([content length]) + + if (!calendar && [content isNotNull]) { - // TODO : c_content could have already been parsed. - // @see _flattenCycleRecord:forRange:intoArray: - elements = [iCalCalendar parseFromSource: content]; - if ([elements count]) - { - components = [[elements objectAtIndex: 0] allObjects]; - max = [components count]; - for (count = 1; count < max; count++) // skip master event - [self _appendCycleException: [components objectAtIndex: count] - firstInstanceCalendarDateRange: fir - fromRow: row - forRange: dateRange - withTimeZone: tz - toArray: ma]; - } + calendar = [iCalCalendar parseSingleFromSource: content]; + } + + if (calendar) + { + components = [calendar allObjects]; + max = [components count]; + for (count = 1; count < max; count++) // skip master event + [self _appendCycleException: [components objectAtIndex: count] + firstInstanceCalendarDateRange: fir + fromRow: row + forRange: dateRange + withTimeZone: tz + toArray: ma]; } } @@ -1068,21 +1130,23 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir * @param theRecords the array into which are copied the resulting occurrences. * @see [iCalRepeatableEntityObject+SOGo doesOccurOnDate:] */ -- (void) _flattenCycleRecord: (NSDictionary *) theRecord - forRange: (NGCalendarDateRange *) theRange - intoArray: (NSMutableArray *) theRecords +- (void) flattenCycleRecord: (NSDictionary *) theRecord + forRange: (NGCalendarDateRange *) theRange + intoArray: (NSMutableArray *) theRecords + withCalendar: (iCalCalendar *) calendar + { NSMutableDictionary *row, *fixedRow; NSMutableArray *records; NSDictionary *cycleinfo; NGCalendarDateRange *firstRange, *recurrenceRange, *oneRange; NSArray *rules, *exRules, *exDates, *ranges; - NSArray *elements, *components; + NSArray *components; NSString *content; NSCalendarDate *checkStartDate, *checkEndDate, *firstStartDate, *firstEndDate; NSTimeZone *allDayTimeZone; iCalDateTime *dtstart; - iCalEvent *component; + iCalRepeatableEntityObject *component; iCalTimeZone *eventTimeZone; unsigned count, max, offset; id tz; @@ -1109,104 +1173,122 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir allDayTimeZone = nil; tz = nil; - row = [self fixupRecord: theRecord]; + row = [self _fixupRecord: theRecord]; [row removeObjectForKey: @"c_cycleinfo"]; [row setObject: sharedYes forKey: @"isRecurrentEvent"]; content = [theRecord objectForKey: @"c_content"]; - if ([content isNotNull]) + + if (!calendar && [content isNotNull]) { - elements = [iCalCalendar parseFromSource: content]; - if ([elements count]) - { - components = [[elements objectAtIndex: 0] events]; - if ([components count]) - { - // Retrieve the range of the first/master event - component = [components objectAtIndex: 0]; - dtstart = (iCalDateTime *) [component uniqueChildWithTag: @"dtstart"]; - firstRange = [component firstOccurenceRange]; // ignores timezone + calendar = [iCalCalendar parseSingleFromSource: content]; + } - eventTimeZone = [dtstart timeZone]; - if (eventTimeZone) - { - // Adjust the range to check with respect to the event timezone (extracted from the start date) - checkStartDate = [eventTimeZone computedDateForDate: [theRange startDate]]; - checkEndDate = [eventTimeZone computedDateForDate: [theRange endDate]]; - recurrenceRange = [NGCalendarDateRange calendarDateRangeWithStartDate: checkStartDate - endDate: checkEndDate]; - - } - else - { - recurrenceRange = theRange; - if ([[theRecord objectForKey: @"c_isallday"] boolValue]) - { - // The event lasts all-day and has no timezone (floating); we convert the range of the first event - // to the user's timezone - allDayTimeZone = timeZone; - offset = [allDayTimeZone secondsFromGMTForDate: [firstRange startDate]]; - firstStartDate = [[firstRange startDate] dateByAddingYears:0 months:0 days:0 hours:0 minutes:0 - seconds:-offset]; - firstEndDate = [[firstRange endDate] dateByAddingYears:0 months:0 days:0 hours:0 minutes:0 - seconds:-offset]; - [firstStartDate setTimeZone: allDayTimeZone]; - [firstEndDate setTimeZone: allDayTimeZone]; - firstRange = [NGCalendarDateRange calendarDateRangeWithStartDate: firstStartDate - endDate: firstEndDate]; - } - } - -#warning this code is ugly: we should not mix objects with different types as\ - it reduces readability - tz = eventTimeZone ? eventTimeZone : allDayTimeZone; - if (tz) - { - // Adjust the exception dates - exDates = [component exceptionDatesWithTimeZone: tz]; - - // Adjust the recurrence rules "until" dates - rules = [component recurrenceRulesWithTimeZone: tz]; - exRules = [component exceptionRulesWithTimeZone: tz]; - } - - // Calculate the occurrences for the given range - records = [NSMutableArray array]; - ranges = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: recurrenceRange - firstInstanceCalendarDateRange: firstRange - recurrenceRules: rules - exceptionRules: exRules - exceptionDates: exDates]; - max = [ranges count]; - for (count = 0; count < max; count++) - { - oneRange = [ranges objectAtIndex: count]; - fixedRow = [self fixupCycleRecord: row - cycleRange: oneRange - firstInstanceCalendarDateRange: firstRange - withEventTimeZone: eventTimeZone]; - if (fixedRow) - [records addObject: fixedRow]; - } + if (calendar) + { + if ([[theRecord objectForKey: @"c_component"] isEqualToString: @"vtodo"]) + components = [calendar todos]; + else + components = [calendar events]; + + if ([components count]) + { + // Retrieve the range of the first/master event + component = [components objectAtIndex: 0]; + dtstart = (iCalDateTime *) [component uniqueChildWithTag: @"dtstart"]; + firstRange = [component firstOccurenceRange]; // ignores timezone + + eventTimeZone = [dtstart timeZone]; + if (eventTimeZone) + { + // Adjust the range to check with respect to the event timezone (extracted from the start date) + checkStartDate = [eventTimeZone computedDateForDate: [theRange startDate]]; + checkEndDate = [eventTimeZone computedDateForDate: [theRange endDate]]; + recurrenceRange = [NGCalendarDateRange calendarDateRangeWithStartDate: checkStartDate + endDate: checkEndDate]; - [self _appendCycleExceptionsFromRow: row - firstInstanceCalendarDateRange: firstRange - forRange: theRange - withTimeZone: allDayTimeZone - toArray: records]; - - [theRecords addObjectsFromArray: records]; } - } + else + { + recurrenceRange = theRange; + if ([[theRecord objectForKey: @"c_isallday"] boolValue]) + { + // The event lasts all-day and has no timezone (floating); we convert the range of the first event + // to the user's timezone + allDayTimeZone = timeZone; + offset = [allDayTimeZone secondsFromGMTForDate: [firstRange startDate]]; + firstStartDate = [[firstRange startDate] dateByAddingYears:0 months:0 days:0 hours:0 minutes:0 + seconds:-offset]; + firstEndDate = [[firstRange endDate] dateByAddingYears:0 months:0 days:0 hours:0 minutes:0 + seconds:-offset]; + [firstStartDate setTimeZone: allDayTimeZone]; + [firstEndDate setTimeZone: allDayTimeZone]; + firstRange = [NGCalendarDateRange calendarDateRangeWithStartDate: firstStartDate + endDate: firstEndDate]; + } + } + +#warning this code is ugly: we should not mix objects with different types as \ + it reduces readability + tz = eventTimeZone ? eventTimeZone : allDayTimeZone; + if (tz) + { + // Adjust the exception dates + exDates = [component exceptionDatesWithTimeZone: tz]; + + // Adjust the recurrence rules "until" dates + rules = [component recurrenceRulesWithTimeZone: tz]; + exRules = [component exceptionRulesWithTimeZone: tz]; + } + + // Calculate the occurrences for the given range + records = [NSMutableArray array]; + ranges = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange: recurrenceRange + firstInstanceCalendarDateRange: firstRange + recurrenceRules: rules + exceptionRules: exRules + exceptionDates: exDates]; + max = [ranges count]; + for (count = 0; count < max; count++) + { + oneRange = [ranges objectAtIndex: count]; + fixedRow = [self fixupCycleRecord: row + cycleRange: oneRange + firstInstanceCalendarDateRange: firstRange + withEventTimeZone: eventTimeZone]; + + // We now adjust the c_nextalarm based on each occurences. For each of them, we use the master event + // alarm information since exceptions to recurrence rules might have their own, while that is not the + // case for standard occurences. + if ([component hasAlarms]) + { + [self _computeAlarmForRow: fixedRow + master: component]; + } + + [records addObject: fixedRow]; + } + + [self _appendCycleExceptionsFromRow: row + firstInstanceCalendarDateRange: firstRange + forRange: theRange + withTimeZone: allDayTimeZone + withCalendar: calendar + toArray: records]; + + [theRecords addObjectsFromArray: records]; + } // if ([components count]) ... } else [self errorWithFormat:@"cyclic record doesn't have content -> %@", theRecord]; } +// +// TODO: is the result supposed to be sorted by date? +// - (NSArray *) _flattenCycleRecords: (NSArray *) _records fetchRange: (NGCalendarDateRange *) _r { - // TODO: is the result supposed to be sorted by date? NSMutableArray *ma; NSDictionary *row; NSCalendarDate *rangeEndDate; @@ -1224,12 +1306,15 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir for (count = 0; count < max; count++) { row = [_records objectAtIndex: count]; - [self _flattenCycleRecord: row forRange: _r intoArray: ma]; + [self flattenCycleRecord: row forRange: _r intoArray: ma withCalendar: nil]; } return ma; } +// +// +// - (void) _buildStripFieldsFromFields: (NSArray *) fields { stripFields = [[NSMutableArray alloc] initWithCapacity: [fields count]]; @@ -1246,6 +1331,9 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir @"c_component", nil]]; } +// +// +// - (void) _fixupProtectedInformation: (NSEnumerator *) ma inFields: (NSArray *) fields forUser: (NSString *) uid @@ -1299,7 +1387,7 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir BOOL rememberRecords, canCycle; rememberRecords = [self _checkIfWeCanRememberRecords: _fields]; - canCycle = [_component isEqualToString: @"vevent"]; + canCycle = [_component isEqualToString: @"vevent"] || [_component isEqualToString: @"vtodo"]; // if (rememberRecords) // NSLog (@"we will remember those records!"); @@ -1341,28 +1429,31 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir } privacySQLString = [self aclSQLListingFilter]; + if (privacySQLString) { if ([privacySQLString length]) [baseWhere addObject: privacySQLString]; - + if ([title length]) - if ([filters length]) { - if ([filters isEqualToString:@"title_Category_Location"] || [filters isEqualToString:@"entireContent"]) - { - [baseWhere addObject: [NSString stringWithFormat: @"(c_title isCaseInsensitiveLike: '%%%@%%' OR c_category isCaseInsensitiveLike: '%%%@%%' OR c_location isCaseInsensitiveLike: '%%%@%%')", - [title stringByReplacingString: @"'" withString: @"\\'\\'"], - [title stringByReplacingString: @"'" withString: @"\\'\\'"], - [title stringByReplacingString: @"'" withString: @"\\'\\'"]]]; - } + if ([filters length]) + { + if ([filters isEqualToString:@"title_Category_Location"] || [filters isEqualToString:@"entireContent"]) + { + [baseWhere addObject: [NSString stringWithFormat: @"(c_title isCaseInsensitiveLike: '%%%@%%' OR c_category isCaseInsensitiveLike: '%%%@%%' OR c_location isCaseInsensitiveLike: '%%%@%%')", + [title stringByReplacingString: @"'" withString: @"\\'\\'"], + [title stringByReplacingString: @"'" withString: @"\\'\\'"], + [title stringByReplacingString: @"'" withString: @"\\'\\'"]]]; + } + } + else + [baseWhere addObject: [NSString stringWithFormat: @"c_title isCaseInsensitiveLike: '%%%@%%'", + [title stringByReplacingString: @"'" withString: @"\\'\\'"]]]; } - else - [baseWhere addObject: [NSString stringWithFormat: @"c_title isCaseInsensitiveLike: '%%%@%%'", - [title stringByReplacingString: @"'" withString: @"\\'\\'"]]]; - + /* prepare mandatory fields */ - + fields = [NSMutableArray arrayWithArray: _fields]; [fields addObjectUniquely: @"c_name"]; [fields addObjectUniquely: @"c_uid"]; @@ -1386,7 +1477,7 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir if (records) { if (r) - records = [self fixupRecords: records]; + records = [self _fixupRecords: records]; ma = [NSMutableArray arrayWithArray: records]; } else @@ -1971,26 +2062,6 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir return [name isEqualToString: @"OPTIONS"]; } -/* -- (id) lookupComponentByUID: (NSString *) uid -{ - NSString *filename; - id component; - - filename = [self resourceNameForEventUID: uid]; - if (filename) - { - component = [self lookupName: filename inContext: context acquire: NO]; - if ([component isKindOfClass: [NSException class]]) - component = nil; - } - else - component = nil; - - return nil; -} -*/ - - (id) lookupName: (NSString *)_key inContext: (id)_ctx acquire: (BOOL)_flag @@ -3356,5 +3427,127 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir return activeTasks; } +- (void) findEntityForClosestAlarm: (id *) theEntity + timezone: (NSTimeZone *) theTimeZone + startDate: (NSCalendarDate **) theStartDate + endDate: (NSCalendarDate **) theEndDate +{ + // If the event is recurring, we MUST find the right occurence. + if ([*theEntity hasRecurrenceRules]) + { + NSCalendarDate *startDate, *endDate; + NSMutableDictionary *quickRecord; + NSCalendarDate *start, *end; + NGCalendarDateRange *range; + NSMutableArray *alarms; + iCalDateTime *date; + iCalTimeZone *tz; + + BOOL b, isEvent; + + isEvent = [*theEntity isKindOfClass: [iCalEvent class]]; + b = NO; + + if (isEvent) + b = [*theEntity isAllDay]; + + // We build a fake "quick record". Our record must include some mandatory info, like @"c_startdate" and @"c_enddate" + quickRecord = [NSMutableDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool: b], @"c_isallday", + [NSNumber numberWithBool: [*theEntity isRecurrent]], @"c_iscycle", + nil]; + startDate = [*theEntity startDate]; + endDate = (isEvent ? [*theEntity endDate] : [*theEntity due]); + + if ([startDate isNotNull]) + { + if (b) + { + // An all-day event usually doesn't have a timezone associated to its + // start date; however, if it does, we convert it to GMT. + date = (iCalDateTime*) [*theEntity uniqueChildWithTag: @"dtstart"]; + tz = [(iCalDateTime*) date timeZone]; + if (tz) + startDate = [tz computedDateForDate: startDate]; + } + [quickRecord setObject: [*theEntity quickRecordDateAsNumber: startDate + withOffset: 0 + forAllDay: b] + forKey: @"c_startdate"]; + } + + if ([endDate isNotNull]) + { + if (b) + { + // An all-day event usually doesn't have a timezone associated to its + // end date; however, if it does, we convert it to GMT. + date = (isEvent ? (iCalDateTime*) [*theEntity uniqueChildWithTag: @"dtend"] : (iCalDateTime*) [*theEntity uniqueChildWithTag: @"due"]); + tz = [(iCalDateTime*) date timeZone]; + if (tz) + endDate = [tz computedDateForDate: endDate]; + } + [quickRecord setObject: [*theEntity quickRecordDateAsNumber: endDate + withOffset: ((b) ? -1 : 0) + forAllDay: b] + forKey: @"c_enddate"]; + } + + + if ([*theEntity isRecurrent]) + { + NSCalendarDate *date; + + date = [*theEntity lastPossibleRecurrenceStartDate]; + if (!date) + { + /* this could also be *nil*, but in the end it makes the fetchspecs + more complex - thus we set it to a "reasonable" distant future */ + date = iCalDistantFuture; + } + [quickRecord setObject: [*theEntity quickRecordDateAsNumber: date + withOffset: 0 forAllDay: NO] + forKey: @"c_cycleenddate"]; + [quickRecord setObject: [*theEntity cycleInfo] forKey: @"c_cycleinfo"]; + } + + alarms = [NSMutableArray array]; + start = [NSCalendarDate date]; + end = [start addYear:1 month:0 day:0 hour:0 minute:0 second:0]; + range = [NGCalendarDateRange calendarDateRangeWithStartDate: start + endDate: end]; + + [self flattenCycleRecord: quickRecord + forRange: range + intoArray: alarms + withCalendar: [*theEntity parent]]; + + if ([alarms count]) + { + NSDictionary *anAlarm; + id o; + + // Take the first alarm since it's the 'closest' one + anAlarm = [alarms objectAtIndex: 0]; + + // We grab the last one and we use that info. The logic is simple. + // 1. grab the RECURRENCE-ID, if found in our master event, use that + // 2. if not found, use the master's event info but adjust the start/end date + if ((o = [[*theEntity parent] eventWithRecurrenceID: [anAlarm objectForKey: @"c_recurrence_id"]])) + { + *theEntity = o; + *theStartDate = [*theEntity startDate]; + *theEndDate = [*theEntity endDate]; + } + else + { + *theStartDate = [NSCalendarDate dateWithTimeIntervalSince1970: [[anAlarm objectForKey: @"c_startdate"] intValue]]; + *theEndDate = [NSCalendarDate dateWithTimeIntervalSince1970: [[anAlarm objectForKey: @"c_enddate"] intValue]]; + } + + [*theStartDate setTimeZone: theTimeZone]; + [*theEndDate setTimeZone: theTimeZone]; + } + } // if ([event hasRecurrenceRules]) ... +} @end /* SOGoAppointmentFolder */ diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index 986978878..80c6700f3 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -905,7 +905,7 @@ inRecurrenceExceptionsForEvent: (iCalEvent *) theEvent updatedAttendees: nil operation: EventCreated]; } - else + else { BOOL hasOrganizer; diff --git a/SoObjects/Appointments/SOGoAppointmentOccurence.h b/SoObjects/Appointments/SOGoAppointmentOccurence.h index b9e2b09c1..fec12416a 100644 --- a/SoObjects/Appointments/SOGoAppointmentOccurence.h +++ b/SoObjects/Appointments/SOGoAppointmentOccurence.h @@ -1,8 +1,6 @@ /* SOGoAppointmentOccurence.h - this file is part of SOGo * - * Copyright (C) 2008 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2008-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/SOGoAppointmentOccurence.m b/SoObjects/Appointments/SOGoAppointmentOccurence.m index 0c3ab2370..75d396072 100644 --- a/SoObjects/Appointments/SOGoAppointmentOccurence.m +++ b/SoObjects/Appointments/SOGoAppointmentOccurence.m @@ -1,8 +1,6 @@ /* SOGoAppointmentOccurence.m - this file is part of SOGo * - * Copyright (C) 2008-2012 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2008-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/SOGoComponentOccurence.h b/SoObjects/Appointments/SOGoComponentOccurence.h index b0c9583c9..68b1d5369 100644 --- a/SoObjects/Appointments/SOGoComponentOccurence.h +++ b/SoObjects/Appointments/SOGoComponentOccurence.h @@ -1,8 +1,6 @@ /* SOGoComponentOccurence.h - this file is part of SOGo * - * Copyright (C) 2008 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2008-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -56,6 +54,7 @@ inContainer: (SOGoCalendarComponent *) newContainer; - (void) setComponent: (iCalRepeatableEntityObject *) newComponent; +- (iCalRepeatableEntityObject *) masterComponent; - (void) setMasterComponent: (iCalRepeatableEntityObject *) newMaster; @end diff --git a/SoObjects/Appointments/SOGoComponentOccurence.m b/SoObjects/Appointments/SOGoComponentOccurence.m index 0609720b3..3b829d15e 100644 --- a/SoObjects/Appointments/SOGoComponentOccurence.m +++ b/SoObjects/Appointments/SOGoComponentOccurence.m @@ -127,6 +127,12 @@ ASSIGN (parentCalendar, [component parent]); } + +- (iCalRepeatableEntityObject *) masterComponent +{ + return master; +} + - (void) setMasterComponent: (iCalRepeatableEntityObject *) newMaster { master = newMaster; @@ -194,7 +200,7 @@ [master increaseSequence]; // We save the updated iCalendar in the database. - error = [container saveComponent: calendar]; + error = [container saveCalendar: calendar]; } return error; diff --git a/SoObjects/Appointments/SOGoEMailAlarmsManager.h b/SoObjects/Appointments/SOGoEMailAlarmsManager.h index ca3df303e..3f9e21ffa 100644 --- a/SoObjects/Appointments/SOGoEMailAlarmsManager.h +++ b/SoObjects/Appointments/SOGoEMailAlarmsManager.h @@ -1,8 +1,6 @@ /* SOGoEMailAlarmsManager.h - this file is part of SOGo * - * Copyright (C) 2010 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2010-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/SOGoEMailAlarmsManager.m b/SoObjects/Appointments/SOGoEMailAlarmsManager.m index cee4b1340..6592b348e 100644 --- a/SoObjects/Appointments/SOGoEMailAlarmsManager.m +++ b/SoObjects/Appointments/SOGoEMailAlarmsManager.m @@ -1,8 +1,6 @@ /* SOGoEMailAlarmsManager.m - this file is part of SOGo * - * Copyright (C) 2010 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2010-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/SOGoTaskObject.m b/SoObjects/Appointments/SOGoTaskObject.m index a94ca3a38..3e1343726 100644 --- a/SoObjects/Appointments/SOGoTaskObject.m +++ b/SoObjects/Appointments/SOGoTaskObject.m @@ -20,6 +20,8 @@ 02111-1307, USA. */ +#import +#import #import #import @@ -81,4 +83,35 @@ return ex; } +- (iCalRepeatableEntityObject *) newOccurenceWithID: (NSString *) theRecurrenceID +{ + iCalToDo *newOccurence, *master; + NSCalendarDate *date, *firstDate; + NSTimeInterval interval; + + newOccurence = (iCalToDo *) [super newOccurenceWithID: theRecurrenceID]; + date = [newOccurence recurrenceId]; + + master = [self component: NO secure: NO]; + firstDate = [master startDate]; + + interval = [[master due] + timeIntervalSinceDate: (NSDate *)firstDate]; + + [newOccurence setStartDate: date]; + [newOccurence setDue: [date addYear: 0 + month: 0 + day: 0 + hour: 0 + minute: 0 + second: interval]]; + + return newOccurence; +} + +- (void) prepareDeleteOccurence: (iCalToDo *) occurence +{ + +} + @end /* SOGoTaskObject */ diff --git a/SoObjects/Appointments/SOGoTaskOccurence.h b/SoObjects/Appointments/SOGoTaskOccurence.h index 80a6172bc..112424f91 100644 --- a/SoObjects/Appointments/SOGoTaskOccurence.h +++ b/SoObjects/Appointments/SOGoTaskOccurence.h @@ -1,8 +1,6 @@ /* SOGoTaskOccurence.h - this file is part of SOGo * - * Copyright (C) 2008 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2008-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/SOGoTaskOccurence.m b/SoObjects/Appointments/SOGoTaskOccurence.m index 129591c35..e964ff12b 100644 --- a/SoObjects/Appointments/SOGoTaskOccurence.m +++ b/SoObjects/Appointments/SOGoTaskOccurence.m @@ -1,8 +1,6 @@ /* SOGoTaskOccurence.m - this file is part of SOGo * - * Copyright (C) 2008 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2008-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/SoObjects/Appointments/iCalCalendar+SOGo.m b/SoObjects/Appointments/iCalCalendar+SOGo.m index e3af84d15..74d660835 100644 --- a/SoObjects/Appointments/iCalCalendar+SOGo.m +++ b/SoObjects/Appointments/iCalCalendar+SOGo.m @@ -25,6 +25,7 @@ #import #import "iCalCalendar+SOGo.h" +#import "iCalEntityObject+SOGo.h" @implementation iCalCalendar (SOGoExtensions) @@ -78,7 +79,8 @@ return [self _occurrence: recID inArray: [self todos]]; } -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer +- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent + container: (id) theContainer { CardGroup *element; NSArray *elements; @@ -91,11 +93,11 @@ element = [elements objectAtIndex: 0]; else { - [self logWithFormat: @"ERROR: given calendar contains no elements: %@", self]; + NSLog(@"ERROR: given calendar contains no elements: %@", self); element = nil; } - return [element quickRecordForContainer: theContainer]; + return [(id)element quickRecordFromContent: theContent container: theContainer]; } @end diff --git a/SoObjects/Appointments/iCalEntityObject+SOGo.h b/SoObjects/Appointments/iCalEntityObject+SOGo.h index a37c727d5..abe9e8e43 100644 --- a/SoObjects/Appointments/iCalEntityObject+SOGo.h +++ b/SoObjects/Appointments/iCalEntityObject+SOGo.h @@ -23,6 +23,7 @@ #import +@class NSMutableDictionary; @class SOGoUser; extern NSCalendarDate *iCalDistantFuture; @@ -43,7 +44,6 @@ extern NSNumber *iCalDistantFutureNumber; - (id) itipEntryWithMethod: (NSString *) method; - (NSArray *) attendeesWithoutUser: (SOGoUser *) user; -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer; - (int) priorityNumber; - (NSString *) createdBy; @@ -52,6 +52,12 @@ extern NSNumber *iCalDistantFutureNumber; withOffset: (int) offset forAllDay: (BOOL) allDay; +- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent + container: (id) theContainer; + +- (void) updateNextAlarmDateInRow: (NSMutableDictionary *) row + forContainer: (id) theContainer; + @end #endif /* ICALENTITYOBJECT_SOGO_H */ diff --git a/SoObjects/Appointments/iCalEntityObject+SOGo.m b/SoObjects/Appointments/iCalEntityObject+SOGo.m index cdb32a582..e0952cb58 100644 --- a/SoObjects/Appointments/iCalEntityObject+SOGo.m +++ b/SoObjects/Appointments/iCalEntityObject+SOGo.m @@ -25,9 +25,16 @@ #import #import +#import #import #import #import +#import + +#import +#import + +#import "SOGoAppointmentFolder.h" #import #import @@ -38,6 +45,7 @@ #import "iCalPerson+SOGo.h" +#import "iCalCalendar+SOGo.h" #import "iCalEntityObject+SOGo.h" NSCalendarDate *iCalDistantFuture = nil; @@ -213,7 +221,8 @@ NSNumber *iCalDistantFutureNumber = nil; return dateNumber; } -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer +- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent + container: (id) theContainer { [self subclassResponsibility: _cmd]; @@ -255,4 +264,139 @@ NSNumber *iCalDistantFutureNumber = nil; return created_by; } +- (void) updateNextAlarmDateInRow: (NSMutableDictionary *) row + forContainer: (id) theContainer +{ + NSCalendarDate *nextAlarmDate; + + nextAlarmDate = nil; + + if ([self hasAlarms]) + { + // We currently have the following limitations for alarms: + // - only the first alarm is considered; + // - the alarm's action must be of type DISPLAY; + // + // Morever, we don't update the quick table if the property X-WebStatus + // of the trigger is set to "triggered". + iCalAlarm *anAlarm; + NSString *webstatus; + + if (![(id)self isRecurrent]) + { + anAlarm = [[self alarms] objectAtIndex: 0]; + if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] == NSOrderedSame) + { + webstatus = [[anAlarm trigger] value: 0 ofAttribute: @"x-webstatus"]; + if (!webstatus + || ([webstatus caseInsensitiveCompare: @"TRIGGERED"] + != NSOrderedSame)) + nextAlarmDate = [anAlarm nextAlarmDate]; + } + else + { + // TODO: handle email alarms here + } + } + // Recurring event/task + else + { + NSCalendarDate *start, *end; + NGCalendarDateRange *range; + NSMutableArray *alarms; + + alarms = [NSMutableArray array]; + start = [NSCalendarDate date]; + end = [start addYear:1 month:0 day:0 hour:0 minute:0 second:0]; + range = [NGCalendarDateRange calendarDateRangeWithStartDate: start + endDate: end]; + + // Always check if container is defined. If not, that means this method + // call was reentrant. + if (theContainer) + { + NSTimeInterval now; + int i, v, delta, c_startdate, c_nextalarm; + + // + // Here is the logic: + // + // We flatten the structure. When flattening it (or after), we compute the alarm based on the trigger for every single + // event part of the recurrence rule. Exceptions can have their own triggers. Then, we start from NOW and move forward, + // and we look at the closest one. When found one, we pick it and store it in c_nextalarm. + // + // When popping up the alarm, we must find for which occurence it is - so we can show the proper start/end time, or even + // infos from the exception. It gets tricky because the user could have snoozed the alarm. So here is the logic: + // + // We flatten the structure and compute the alarms based on triggers. If c_nextalarm is a match, we have have a winner. + // If we don't have a match, we pick the event for which its trigger is the closest to the c_nextalarm. + // + nextAlarmDate = nil; + + [theContainer flattenCycleRecord: (id)row + forRange: range + intoArray: alarms + withCalendar: [self parent]]; + + // We pickup the closest one from now. We remove the actual reminder (ie., 15 mins before - plus 1 minute for roundups) + // so we don't pickup the same alarm over and over. This could happen if our alarm popups and the user clicks on "Cancel". + // In case of a repetitive event, we want to pickup the next one (next day for example), and not the one that has just + // popped up. + now = [start timeIntervalSince1970]; + v = 0; + for (i = 0; i < [alarms count]; i++) + { + c_startdate = [[[alarms objectAtIndex: i] objectForKey: @"c_startdate"] intValue]; + c_nextalarm = [[[alarms objectAtIndex: i] objectForKey: @"c_nextalarm"] intValue]; + delta = (c_startdate - now - (c_startdate - c_nextalarm)) - 60; + + // If value is not initialized, we grab it right away + if (!v && delta > 0) + v = delta; + + // If we found a smaller delta than before, use it. + if (v > 0 && delta > 0 && delta <= v) + { + id o; + + // Find the relevant component + if ([self respondsToSelector: @selector(endDate)]) + o = [[self parent] eventWithRecurrenceID: [[alarms objectAtIndex: i] objectForKey: @"c_recurrence_id"]]; + else + o = [[self parent] todoWithRecurrenceID: [[alarms objectAtIndex: i] objectForKey: @"c_recurrence_id"]]; + + if (!o) + o = self; + + if ([[o alarms] count]) + { + anAlarm = [[o alarms] objectAtIndex: 0]; + if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] == NSOrderedSame) + { + webstatus = [[anAlarm trigger] value: 0 ofAttribute: @"x-webstatus"]; + if (!webstatus + || ([webstatus caseInsensitiveCompare: @"TRIGGERED"] + != NSOrderedSame)) + + v = delta; + nextAlarmDate = [NSDate dateWithTimeIntervalSince1970: [[[alarms objectAtIndex: i] objectForKey: @"c_nextalarm"] intValue]]; + } + else + { + // TODO: handle email alarms here + } + } + } + } // for ( ... ) + } // if (theContainer) + } + } + + if ([nextAlarmDate isNotNull]) + [row setObject: [NSNumber numberWithInt: [nextAlarmDate timeIntervalSince1970]] + forKey: @"c_nextalarm"]; + else + [row setObject: [NSNumber numberWithInt: 0] forKey: @"c_nextalarm"]; +} + @end diff --git a/SoObjects/Appointments/iCalEvent+SOGo.h b/SoObjects/Appointments/iCalEvent+SOGo.h index fd4869132..d04d89427 100644 --- a/SoObjects/Appointments/iCalEvent+SOGo.h +++ b/SoObjects/Appointments/iCalEvent+SOGo.h @@ -29,7 +29,6 @@ - (BOOL) isStillRelevant; - (unsigned int) occurenceInterval; -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer; - (void) updateRecurrenceRulesUntilDate: (NSCalendarDate *) previousEndDate; @end diff --git a/SoObjects/Appointments/iCalEvent+SOGo.m b/SoObjects/Appointments/iCalEvent+SOGo.m index f811ceeca..485d2822e 100644 --- a/SoObjects/Appointments/iCalEvent+SOGo.m +++ b/SoObjects/Appointments/iCalEvent+SOGo.m @@ -28,7 +28,6 @@ #import #import -#import #import #import #import @@ -37,6 +36,7 @@ #import #import +#import "SOGoAppointmentFolder.h" #import "iCalRepeatableEntityObject+SOGo.h" #import "iCalEvent+SOGo.h" @@ -64,10 +64,11 @@ // // // -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer +- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent + container: (id) theContainer { NSMutableDictionary *row; - NSCalendarDate *startDate, *endDate, *nextAlarmDate; + NSCalendarDate *startDate, *endDate; NSArray *attendees, *categories; NSString *uid, *title, *location, *status; NSNumber *sequence; @@ -167,6 +168,7 @@ forAllDay: isAllDay] forKey: @"c_enddate"]; } + if ([self isRecurrent]) { NSCalendarDate *date; @@ -202,11 +204,11 @@ else { /* confirmed by default */ - [row setObject: [NSNumber numberWithInt:1] forKey: @"c_status"]; + [row setObject: [NSNumber numberWithInt: 1] forKey: @"c_status"]; } [row setObject: [NSNumber numberWithUnsignedInt: accessClass] - forKey: @"c_classification"]; + forKey: @"c_classification"]; organizer = [self organizer]; if (organizer) @@ -235,35 +237,10 @@ [row setObject:partstates forKey: @"c_partstates"]; [partstates release]; - nextAlarmDate = nil; - if (![self isRecurrent] && [self hasAlarms]) - { - // We currently have the following limitations for alarms: - // - the component must not be recurrent; - // - only the first alarm is considered; - // - the alarm's action must be of type DISPLAY; - // - // Morever, we don't update the quick table if the property X-WebStatus - // of the trigger is set to "triggered". - iCalAlarm *anAlarm; - NSString *webstatus; - - anAlarm = [[self alarms] objectAtIndex: 0]; - if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] == NSOrderedSame) - { - webstatus = [[anAlarm trigger] value: 0 ofAttribute: @"x-webstatus"]; - if (!webstatus - || ([webstatus caseInsensitiveCompare: @"TRIGGERED"] - != NSOrderedSame)) - nextAlarmDate = [anAlarm nextAlarmDate]; - } - } - if ([nextAlarmDate isNotNull]) - [row setObject: [NSNumber numberWithInt: [nextAlarmDate timeIntervalSince1970]] - forKey: @"c_nextalarm"]; - else - [row setObject: [NSNumber numberWithInt: 0] forKey: @"c_nextalarm"]; + /* handle alarms */ + [self updateNextAlarmDateInRow: row forContainer: theContainer]; + /* handle categories */ categories = [self categories]; if ([categories count] > 0) [row setObject: [categories componentsJoinedByString: @","] @@ -274,33 +251,7 @@ return row; } -/** - * Extract the start and end dates from the event, from which all recurrence - * calculations will be based on. - * @return the range of the first occurrence. - */ -- (NGCalendarDateRange *) firstOccurenceRange -{ - NSCalendarDate *start, *end; - NGCalendarDateRange *firstRange; - NSArray *dates; - - firstRange = nil; - - dates = [[[self uniqueChildWithTag: @"dtstart"] valuesForKey: @""] lastObject]; - if ([dates count] > 0) - { - start = [[dates lastObject] asCalendarDate]; // ignores timezone - end = [start addTimeInterval: [self occurenceInterval]]; - - firstRange = [NGCalendarDateRange calendarDateRangeWithStartDate: start - endDate: end]; - } - - return firstRange; -} - -- (unsigned int) occurenceInterval +- (NSTimeInterval) occurenceInterval { return [[self endDate] timeIntervalSinceDate: [self startDate]]; } diff --git a/SoObjects/Appointments/iCalRepeatableEntityObject+SOGo.h b/SoObjects/Appointments/iCalRepeatableEntityObject+SOGo.h index 845167967..2a2efd427 100644 --- a/SoObjects/Appointments/iCalRepeatableEntityObject+SOGo.h +++ b/SoObjects/Appointments/iCalRepeatableEntityObject+SOGo.h @@ -28,6 +28,7 @@ - (NSString *) cycleInfo; - (NGCalendarDateRange *) firstOccurenceRange; +- (NSTimeInterval) occurenceInterval; - (BOOL) doesOccurOnDate: (NSCalendarDate *) occurenceDate; @end diff --git a/SoObjects/Appointments/iCalRepeatableEntityObject+SOGo.m b/SoObjects/Appointments/iCalRepeatableEntityObject+SOGo.m index 128249e88..c9f66a6b1 100644 --- a/SoObjects/Appointments/iCalRepeatableEntityObject+SOGo.m +++ b/SoObjects/Appointments/iCalRepeatableEntityObject+SOGo.m @@ -23,6 +23,7 @@ #import #import #import +#import #import #import @@ -31,6 +32,7 @@ #import #import #import + #import #import "iCalRepeatableEntityObject+SOGo.h" @@ -98,14 +100,33 @@ return value; } +/** + * Extract the start and end dates from the event, from which all recurrence + * calculations will be based on. + * @return the range of the first occurrence. + */ - (NGCalendarDateRange *) firstOccurenceRange { - [self subclassResponsibility: _cmd]; + NSCalendarDate *start, *end; + NGCalendarDateRange *firstRange; + NSArray *dates; - return nil; + firstRange = nil; + + dates = [[[self uniqueChildWithTag: @"dtstart"] valuesForKey: @""] lastObject]; + if ([dates count] > 0) + { + start = [[dates lastObject] asCalendarDate]; // ignores timezone + end = [start addTimeInterval: [self occurenceInterval]]; + + firstRange = [NGCalendarDateRange calendarDateRangeWithStartDate: start + endDate: end]; + } + + return firstRange; } -- (unsigned int) occurenceInterval +- (NSTimeInterval) occurenceInterval { [self subclassResponsibility: _cmd]; @@ -123,9 +144,8 @@ NSArray *ranges; NGCalendarDateRange *checkRange, *firstRange; NSCalendarDate *startDate, *endDate; - id firstStartDate, firstEndDate, timeZone; + id firstStartDate, timeZone; BOOL doesOccur; - int offset; doesOccur = [self isRecurrent]; if (doesOccur) diff --git a/SoObjects/Appointments/iCalToDo+SOGo.h b/SoObjects/Appointments/iCalToDo+SOGo.h index 8da587395..e381686c9 100644 --- a/SoObjects/Appointments/iCalToDo+SOGo.h +++ b/SoObjects/Appointments/iCalToDo+SOGo.h @@ -27,8 +27,6 @@ @interface iCalToDo (SOGoExtensions) -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer; - @end #endif /* ICALTODO_SOGO_H */ diff --git a/SoObjects/Appointments/iCalToDo+SOGo.m b/SoObjects/Appointments/iCalToDo+SOGo.m index f46f05ba5..80b1194bb 100644 --- a/SoObjects/Appointments/iCalToDo+SOGo.m +++ b/SoObjects/Appointments/iCalToDo+SOGo.m @@ -38,10 +38,11 @@ @implementation iCalToDo (SOGoExtensions) -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer +- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent + container: (id) theContainer { NSMutableDictionary *row; - NSCalendarDate *startDate, *dueDate, *nextAlarmDate; + NSCalendarDate *startDate, *dueDate; NSArray *attendees, *categories; NSString *uid, *title, *location, *status; NSNumber *sequence; @@ -55,7 +56,6 @@ startDate = [self startDate]; dueDate = [self due]; - nextAlarmDate = nil; uid = [self uid]; title = [self summary]; if (![title isNotNull]) @@ -168,35 +168,8 @@ [row setObject:partstates forKey: @"c_partstates"]; [partstates release]; - nextAlarmDate = nil; - if (![self isRecurrent] && [self hasAlarms]) - { - // We currently have the following limitations for alarms: - // - the component must not be recurrent; - // - only the first alarm is considered; - // - the alarm's action must be of type DISPLAY; - // - // Morever, we don't update the quick table if the property X-WebStatus - // of the trigger is set to "triggered". - iCalAlarm *anAlarm; - NSString *webstatus; - - anAlarm = [[self alarms] objectAtIndex: 0]; - if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] - == NSOrderedSame) - { - webstatus = [[anAlarm trigger] value: 0 ofAttribute: @"x-webstatus"]; - if (!webstatus - || ([webstatus caseInsensitiveCompare: @"TRIGGERED"] - != NSOrderedSame)) - nextAlarmDate = [anAlarm nextAlarmDate]; - } - } - if ([nextAlarmDate isNotNull]) - [row setObject: [NSNumber numberWithInt: [nextAlarmDate timeIntervalSince1970]] - forKey: @"c_nextalarm"]; - else - [row setObject: [NSNumber numberWithInt: 0] forKey: @"c_nextalarm"]; + /* handle alarms */ + [self updateNextAlarmDateInRow: row forContainer: theContainer]; categories = [self categories]; if ([categories count] > 0) @@ -206,13 +179,7 @@ return row; } -- (NGCalendarDateRange *) firstOccurenceRange -{ - return [NGCalendarDateRange calendarDateRangeWithStartDate: [self startDate] - endDate: [self due]]; -} - -- (unsigned int) occurenceInterval +- (NSTimeInterval) occurenceInterval { return [[self due] timeIntervalSinceDate: [self startDate]]; } diff --git a/SoObjects/Contacts/NGVCard+SOGo.m b/SoObjects/Contacts/NGVCard+SOGo.m index 5f94a8a87..83e646c06 100644 --- a/SoObjects/Contacts/NGVCard+SOGo.m +++ b/SoObjects/Contacts/NGVCard+SOGo.m @@ -791,7 +791,8 @@ convention: return date; } -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer +- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent + container: (id) theContainer { NSMutableDictionary *fields; CardElement *element; diff --git a/SoObjects/Contacts/NGVList+SOGo.m b/SoObjects/Contacts/NGVList+SOGo.m index 7c7bebbe7..98b38c319 100644 --- a/SoObjects/Contacts/NGVList+SOGo.m +++ b/SoObjects/Contacts/NGVList+SOGo.m @@ -69,7 +69,8 @@ return rc; } -- (NSMutableDictionary *) quickRecordForContainer: (id) theContainer +- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent + container: (id) theContainer { NSMutableDictionary *fields; NSString *value; diff --git a/Tools/SOGoEAlarmsNotifier.h b/Tools/SOGoEAlarmsNotifier.h index 692b5260d..bc34b99c1 100644 --- a/Tools/SOGoEAlarmsNotifier.h +++ b/Tools/SOGoEAlarmsNotifier.h @@ -1,8 +1,6 @@ /* SOGoEAlarmsNotifier.h - this file is part of SOGo * - * Copyright (C) 2010 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2010-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/Tools/SOGoEAlarmsNotifier.m b/Tools/SOGoEAlarmsNotifier.m index 81de6528d..ad8cc8f7a 100644 --- a/Tools/SOGoEAlarmsNotifier.m +++ b/Tools/SOGoEAlarmsNotifier.m @@ -1,8 +1,6 @@ /* SOGoEAlarmsNotifier.m - this file is part of SOGo * - * Copyright (C) 2011-2013 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2011-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/UI/Scheduler/UIxAppointmentEditor.h b/UI/Scheduler/UIxAppointmentEditor.h index daa0193f2..bdb4f1df8 100644 --- a/UI/Scheduler/UIxAppointmentEditor.h +++ b/UI/Scheduler/UIxAppointmentEditor.h @@ -1,6 +1,6 @@ /* UIxAppointmentEditor.h - this file is part of SOGo * - * Copyright (C) 2007-2013 Inverse inc. + * Copyright (C) 2007-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index 5f40e2396..1ec514ec4 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -1,6 +1,6 @@ /* UIxAppointmentEditor.m - this file is part of SOGo * - * Copyright (C) 2007-2013 Inverse inc. + * Copyright (C) 2007-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ #import #import #import +#import #import #import @@ -31,6 +32,7 @@ #import #import #import +#import #import #import @@ -49,8 +51,10 @@ #import #import #import +#import #import #import +#import #import #import #import @@ -470,6 +474,7 @@ SOGoUserDefaults *ud; SOGoCalendarComponent *co; NSString *created_by; + iCalAlarm *anAlarm; BOOL resetAlarm; unsigned int snoozeAlarm; @@ -492,12 +497,26 @@ componentCalendar = [componentCalendar container]; [componentCalendar retain]; } + + created_by = [event createdBy]; - if ([event hasAlarms] && ![event hasRecurrenceRules]) + // resetAlarm=yes is set only when we are about to show the alarm popup in the Web + // interface of SOGo. See generic.js for details. snoozeAlarm=X is called when the + // user clicks on "Snooze for" X minutes, when the popup is being displayed. + // If either is set to yes, we must find the right alarm. + resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue]; + snoozeAlarm = [[[context request] formValueForKey: @"snoozeAlarm"] intValue]; + + if (resetAlarm || snoozeAlarm) { - iCalAlarm *anAlarm; - resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue]; - snoozeAlarm = [[[context request] formValueForKey: @"snoozeAlarm"] intValue]; + iCalEvent *master; + + master = event; + [componentCalendar findEntityForClosestAlarm: &event + timezone: timeZone + startDate: &eventStartDate + endDate: &eventEndDate]; + if (resetAlarm) { iCalTrigger *aTrigger; @@ -506,7 +525,7 @@ aTrigger = [anAlarm trigger]; [aTrigger setValue: 0 ofAttribute: @"x-webstatus" to: @"triggered"]; - [co saveComponent: event]; + [co saveComponent: master]; } else if (snoozeAlarm) { @@ -516,8 +535,6 @@ } } - created_by = [event createdBy]; - data = [NSDictionary dictionaryWithObjectsAndKeys: [[componentCalendar displayName] stringByEscapingHTMLString], @"calendar", [event tag], @"component", @@ -525,7 +542,6 @@ [dateFormatter formattedTime: eventStartDate], @"startTime", [dateFormatter formattedDate: eventEndDate], @"endDate", [dateFormatter formattedTime: eventEndDate], @"endTime", - //([event hasRecurrenceRules] ? @"1": @"0"), @"isRecurring", ([event isAllDay] ? @"1": @"0"), @"isAllDay", [[event summary] stringByEscapingHTMLString], @"summary", [[event location] stringByEscapingHTMLString], @"location", diff --git a/UI/Scheduler/UIxCalListingActions.m b/UI/Scheduler/UIxCalListingActions.m index aa8749fb6..32d2a6a7f 100644 --- a/UI/Scheduler/UIxCalListingActions.m +++ b/UI/Scheduler/UIxCalListingActions.m @@ -99,7 +99,7 @@ static NSArray *tasksFields = nil; @"c_status", @"c_title", @"c_enddate", @"c_classification", @"c_location", @"c_category", @"editable", @"erasable", - @"c_priority", @"c_owner", nil]; + @"c_priority", @"c_owner", @"c_recurrence_id", @"isException", nil]; [tasksFields retain]; } } @@ -331,150 +331,158 @@ static NSArray *tasksFields = nil; folders = [[clientObject subFolders] objectEnumerator]; while ((currentFolder = [folders nextObject])) - { - if ([currentFolder isActive]) { - folderIsRemote = [currentFolder isKindOfClass: [SOGoWebAppointmentFolder class]]; + if ([currentFolder isActive]) + { + folderIsRemote = [currentFolder isKindOfClass: [SOGoWebAppointmentFolder class]]; - if ([criteria isEqualToString:@"title_Category_Location"]) - currentInfos = [[currentFolder fetchCoreInfosFrom: startDate - to: endDate - title: value - component: component - additionalFilters: criteria] objectEnumerator]; - - else if ([criteria isEqualToString:@"entireContent"]) - { - // First research : Through the quick table inside the location, category and title columns - quickInfos = [currentFolder fetchCoreInfosFrom: startDate - to: endDate - title: value - component: component - additionalFilters: criteria]; - - // Save the c_name in another array to compare with - if ([quickInfos count] > 0) - { - quickInfosFlag = YES; - quickInfosName = [NSMutableArray arrayWithCapacity:[quickInfos count]]; - for (i = 0; i < [quickInfos count]; i++) - [quickInfosName addObject:[[quickInfos objectAtIndex:i] objectForKey:@"c_name"]]; - } - - // Second research : Every objects except for those already in the quickInfos array - allInfos = [currentFolder fetchCoreInfosFrom: startDate - to: endDate - title: nil - component: component]; - if (quickInfosFlag == YES) - { - for (i = ([allInfos count] - 1); i >= 0 ; i--) { - if([quickInfosName containsObject:[[allInfos objectAtIndex:i] objectForKey:@"c_name"]]) - [allInfos removeObjectAtIndex:i]; - } - } - - - for (i = 0; i < [allInfos count]; i++) - { - iCalString = [[allInfos objectAtIndex:i] objectForKey:@"c_content"]; - calendar = [iCalCalendar parseSingleFromSource: iCalString]; - master = [calendar firstChildWithTag:component]; - if (master) { - if ([[master comment] length] > 0) + // + // criteria can have the following values: "title", "title_Category_Location" and "entireContent" + // + if ([criteria isEqualToString:@"title_Category_Location"]) { - match = [[master comment] rangeOfString:value options:NSCaseInsensitiveSearch]; - if (match.length > 0) { - [quickInfos addObject:[allInfos objectAtIndex:i]]; - } + currentInfos = [[currentFolder fetchCoreInfosFrom: startDate + to: endDate + title: value + component: component + additionalFilters: criteria] objectEnumerator]; } - } - } + else if ([criteria isEqualToString:@"entireContent"]) + { + // First search : Through the quick table inside the location, category and title columns + quickInfos = [currentFolder fetchCoreInfosFrom: startDate + to: endDate + title: value + component: component + additionalFilters: criteria]; - currentInfos = [quickInfos objectEnumerator]; - } - + // Save the c_name in another array to compare with + if ([quickInfos count] > 0) + { + quickInfosFlag = YES; + quickInfosName = [NSMutableArray arrayWithCapacity:[quickInfos count]]; + for (i = 0; i < [quickInfos count]; i++) + [quickInfosName addObject:[[quickInfos objectAtIndex:i] objectForKey:@"c_name"]]; + } + + // Second research : Every objects except for those already in the quickInfos array + allInfos = [currentFolder fetchCoreInfosFrom: startDate + to: endDate + title: nil + component: component]; + if (quickInfosFlag == YES) + { + for (i = ([allInfos count] - 1); i >= 0 ; i--) { + if([quickInfosName containsObject:[[allInfos objectAtIndex:i] objectForKey:@"c_name"]]) + [allInfos removeObjectAtIndex:i]; + } + } + + + for (i = 0; i < [allInfos count]; i++) + { + iCalString = [[allInfos objectAtIndex:i] objectForKey:@"c_content"]; + calendar = [iCalCalendar parseSingleFromSource: iCalString]; + master = [calendar firstChildWithTag:component]; + if (master) { + if ([[master comment] length] > 0) + { + match = [[master comment] rangeOfString:value options:NSCaseInsensitiveSearch]; + if (match.length > 0) { + [quickInfos addObject:[allInfos objectAtIndex:i]]; + } + } + } + } + + currentInfos = [quickInfos objectEnumerator]; + } + else + { + id foo; - else - currentInfos = [[currentFolder fetchCoreInfosFrom: startDate - to: endDate - title: value - component: component] objectEnumerator]; + foo = [currentFolder fetchCoreInfosFrom: startDate + to: endDate + title: value + component: component]; + currentInfos = [foo objectEnumerator]; + } - owner = [currentFolder ownerInContext: context]; - ownerUser = [SOGoUser userWithLogin: owner]; - isErasable = ([owner isEqualToString: userLogin] || [[currentFolder aclsForUser: userLogin] containsObject: SOGoRole_ObjectEraser]); + owner = [currentFolder ownerInContext: context]; + ownerUser = [SOGoUser userWithLogin: owner]; + isErasable = ([owner isEqualToString: userLogin] || [[currentFolder aclsForUser: userLogin] containsObject: SOGoRole_ObjectEraser]); - while ((newInfo = [currentInfos nextObject])) - { - if ([fields containsObject: @"editable"]) - { - if (folderIsRemote) - // .ics subscriptions are not editable - [newInfo setObject: [NSNumber numberWithInt: 0] - forKey: @"editable"]; - else - { - // Identifies whether the active user can edit the event. - role = [currentFolder roleForComponentsWithAccessClass:[[newInfo objectForKey: @"c_classification"] intValue] - forUser: userLogin]; - if ([role isEqualToString: @"ComponentModifier"] || [role length] == 0) - [newInfo setObject: [NSNumber numberWithInt: 1] - forKey: @"editable"]; - else - [newInfo setObject: [NSNumber numberWithInt: 0] - forKey: @"editable"]; - } - } + while ((newInfo = [currentInfos nextObject])) + { + if ([fields containsObject: @"editable"]) + { + if (folderIsRemote) + // .ics subscriptions are not editable + [newInfo setObject: [NSNumber numberWithInt: 0] + forKey: @"editable"]; + else + { + // Identifies whether the active user can edit the event. + role = [currentFolder roleForComponentsWithAccessClass:[[newInfo objectForKey: @"c_classification"] intValue] + forUser: userLogin]; + if ([role isEqualToString: @"ComponentModifier"] || [role length] == 0) + [newInfo setObject: [NSNumber numberWithInt: 1] + forKey: @"editable"]; + else + [newInfo setObject: [NSNumber numberWithInt: 0] + forKey: @"editable"]; + } + } if ([fields containsObject: @"ownerIsOrganizer"]) - { - // Identifies whether the active user is the organizer - // of this event. - NSString *c_orgmail; - c_orgmail = [newInfo objectForKey: @"c_orgmail"]; + { + // Identifies whether the active user is the organizer + // of this event. + NSString *c_orgmail; + c_orgmail = [newInfo objectForKey: @"c_orgmail"]; - if ([c_orgmail isKindOfClass: [NSString class]] && [ownerUser hasEmail: c_orgmail]) - [newInfo setObject: [NSNumber numberWithInt: 1] - forKey: @"ownerIsOrganizer"]; - else - [newInfo setObject: [NSNumber numberWithInt: 0] - forKey: @"ownerIsOrganizer"]; - } + if ([c_orgmail isKindOfClass: [NSString class]] && [ownerUser hasEmail: c_orgmail]) + [newInfo setObject: [NSNumber numberWithInt: 1] + forKey: @"ownerIsOrganizer"]; + else + [newInfo setObject: [NSNumber numberWithInt: 0] + forKey: @"ownerIsOrganizer"]; + } if (isErasable) - [newInfo setObject: [NSNumber numberWithInt: 1] - forKey: @"erasable"]; + [newInfo setObject: [NSNumber numberWithInt: 1] + forKey: @"erasable"]; else - [newInfo setObject: [NSNumber numberWithInt: 0] - forKey: @"erasable"]; + [newInfo setObject: [NSNumber numberWithInt: 0] + forKey: @"erasable"]; [newInfo setObject: [currentFolder nameInContainer] - forKey: @"c_folder"]; - [newInfo setObject: [currentFolder ownerInContext: context] - forKey: @"c_owner"]; - calendarName = [currentFolder displayName]; - if (calendarName == nil) - calendarName = @""; - [newInfo setObject: calendarName - forKey: @"calendarName"]; - if (![[newInfo objectForKey: @"c_title"] length]) - [self _fixComponentTitle: newInfo withType: component]; + forKey: @"c_folder"]; + [newInfo setObject: [currentFolder ownerInContext: context] + forKey: @"c_owner"]; + calendarName = [currentFolder displayName]; + if (calendarName == nil) + calendarName = @""; + [newInfo setObject: calendarName + forKey: @"calendarName"]; + if (![[newInfo objectForKey: @"c_title"] length]) + [self _fixComponentTitle: newInfo withType: component]; + // Possible improvement: only call _fixDates if event is recurrent // or the view range span a daylight saving time change - [self _fixDates: newInfo]; - newInfoForComponent = [NSMutableArray arrayWithArray: [newInfo objectsForKeys: fields - notFoundMarker: marker]]; - // Escape HTML - count = [newInfoForComponent count]; - for (i = 0; i < count; i++) - { - currentInfo = [newInfoForComponent objectAtIndex: i]; - if ([currentInfo respondsToSelector: @selector (stringByEscapingHTMLString)]) - [newInfoForComponent replaceObjectAtIndex: i withObject: [currentInfo stringByEscapingHTMLString]]; + [self _fixDates: newInfo]; + newInfoForComponent = [NSMutableArray arrayWithArray: [newInfo objectsForKeys: fields + notFoundMarker: marker]]; + // Escape HTML + count = [newInfoForComponent count]; + for (i = 0; i < count; i++) + { + currentInfo = [newInfoForComponent objectAtIndex: i]; + if ([currentInfo respondsToSelector: @selector (stringByEscapingHTMLString)]) + [newInfoForComponent replaceObjectAtIndex: i withObject: [currentInfo stringByEscapingHTMLString]]; + } + [infos addObject: newInfoForComponent]; + } } - [infos addObject: newInfoForComponent]; - } } - } return infos; } @@ -520,7 +528,6 @@ static NSArray *tasksFields = nil; // - for ALL subscribed and ACTIVE calendars // - returns alarms that will occur in the next 48 hours or the non-triggered alarms // for non-completed events -// - recurring events are currently ignored // - (WOResponse *) alarmsListAction { @@ -539,33 +546,28 @@ static NSArray *tasksFields = nil; folders = [[clientObject subFolders] objectEnumerator]; while ((currentFolder = [folders nextObject])) - { - if ([currentFolder isActive] && [currentFolder showCalendarAlarms]) { - NSDictionary *entry; - NSArray *alarms; - BOOL isCycle; - int i; - - alarms = [currentFolder fetchAlarmInfosFrom: [NSNumber numberWithInt: browserTime] - to: [NSNumber numberWithInt: laterTime]]; - - for (i = 0; i < [alarms count]; i++) - { - entry = [alarms objectAtIndex: i]; - isCycle = [[entry objectForKey: @"c_iscycle"] boolValue]; - - if (!isCycle) + if ([currentFolder isActive] && [currentFolder showCalendarAlarms]) { - [allAlarms addObject: [NSArray arrayWithObjects: - [currentFolder nameInContainer], - [entry objectForKey: @"c_name"], - [entry objectForKey: @"c_nextalarm"], - nil]]; + NSDictionary *entry; + NSArray *alarms; + int i; + + alarms = [currentFolder fetchAlarmInfosFrom: [NSNumber numberWithInt: browserTime] + to: [NSNumber numberWithInt: laterTime]]; + + for (i = 0; i < [alarms count]; i++) + { + entry = [alarms objectAtIndex: i]; + + [allAlarms addObject: [NSArray arrayWithObjects: + [currentFolder nameInContainer], + [entry objectForKey: @"c_name"], + [entry objectForKey: @"c_nextalarm"], + nil]]; + } } - } } - } response = [self responseWithStatus: 200]; @@ -1242,9 +1244,9 @@ _computeBlocksPosition (NSArray *blocks) [filteredTasks addObject: filteredTask]; else if ([tasksView isEqualToString:@"view_all"]) [filteredTasks addObject: filteredTask]; - else if (([tasksView isEqualToString:@"view_overdue"]) && ([[filteredTask objectAtIndex:12] isEqualToString:@"overdue"])) + else if (([tasksView isEqualToString:@"view_overdue"]) && ([[filteredTask objectAtIndex:15] isEqualToString:@"overdue"])) [filteredTasks addObject: filteredTask]; - else if ([tasksView isEqualToString:@"view_incomplete"] && (![[filteredTask objectAtIndex:12] isEqualToString:@"completed"])) + else if ([tasksView isEqualToString:@"view_incomplete"] && (![[filteredTask objectAtIndex:15] isEqualToString:@"completed"])) [filteredTasks addObject: filteredTask]; else if ([tasksView isEqualToString:@"view_not_started"] && ([[[filteredTask objectAtIndex:3] stringValue] isEqualToString:@"0"])) [filteredTasks addObject: filteredTask]; diff --git a/UI/Scheduler/UIxComponentEditor.h b/UI/Scheduler/UIxComponentEditor.h index ca2eca4ea..6120901d8 100644 --- a/UI/Scheduler/UIxComponentEditor.h +++ b/UI/Scheduler/UIxComponentEditor.h @@ -176,7 +176,6 @@ ////////////////////////////////// JUNK //////////////////////////////////////// /* access */ -- (BOOL) isMyComponent; - (BOOL) canEditComponent; - (unsigned int) firstDayOfWeek; diff --git a/UI/Scheduler/UIxComponentEditor.m b/UI/Scheduler/UIxComponentEditor.m index 4699e929b..1da251cf5 100644 --- a/UI/Scheduler/UIxComponentEditor.m +++ b/UI/Scheduler/UIxComponentEditor.m @@ -1,9 +1,6 @@ /* UIxComponentEditor.m - this file is part of SOGo * - * Copyright (C) 2006-2013 Inverse inc. - * - * Author: Wolfgang Sourdeau - * Francis Lachapelle + * Copyright (C) 2006-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,10 +30,15 @@ #import #import +#import +#import #import #import #import +#import #import + + #import #import #import @@ -46,6 +48,7 @@ #import #import #import +#import #import #import @@ -1717,14 +1720,9 @@ RANGE(2); /* access */ -- (BOOL) isMyComponent -{ - return ([[context activeUser] hasEmail: [organizer rfc822Email]]); -} - - (BOOL) canEditComponent { - return [self isMyComponent]; + return ([[context activeUser] hasEmail: [organizer rfc822Email]]); } /* response generation */ diff --git a/UI/Scheduler/UIxOccurenceDialog.m b/UI/Scheduler/UIxOccurenceDialog.m index 1a573f71e..a3a3096c5 100644 --- a/UI/Scheduler/UIxOccurenceDialog.m +++ b/UI/Scheduler/UIxOccurenceDialog.m @@ -1,8 +1,6 @@ /* UIxOccurenceDialog.m - this file is part of SOGo * - * Copyright (C) 2008 Inverse inc. - * - * Author: Wolfgang Sourdeau + * Copyright (C) 2008-2014 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/UI/Scheduler/UIxTaskEditor.m b/UI/Scheduler/UIxTaskEditor.m index 597e7b83e..d1d447549 100644 --- a/UI/Scheduler/UIxTaskEditor.m +++ b/UI/Scheduler/UIxTaskEditor.m @@ -89,7 +89,7 @@ { if (!todo) { - todo = (iCalToDo *) [[self clientObject] component: YES secure: YES]; + todo = (iCalToDo *) [[self clientObject] occurence]; [[todo parent] retain]; } @@ -424,12 +424,17 @@ WOResponse *result; NSDictionary *data; NSCalendarDate *startDate, *dueDate; + SOGoCalendarComponent *co; NSTimeZone *timeZone; SOGoUserDefaults *ud; + iCalAlarm *anAlarm; BOOL resetAlarm; + BOOL snoozeAlarm; [self todo]; + co = [self clientObject]; + result = [self responseWithStatus: 200]; ud = [[context activeUser] userDefaults]; timeZone = [ud timeZone]; @@ -437,21 +442,41 @@ [startDate setTimeZone: timeZone]; dueDate = [todo due]; [dueDate setTimeZone: timeZone]; - - resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue]; - if (resetAlarm && [todo hasAlarms] && ![todo hasRecurrenceRules]) - { - iCalAlarm *anAlarm; - iCalTrigger *aTrigger; - SOGoCalendarComponent *co; - anAlarm = [[todo alarms] objectAtIndex: 0]; - aTrigger = [anAlarm trigger]; - [aTrigger setValue: 0 ofAttribute: @"x-webstatus" to: @"triggered"]; - - co = [self clientObject]; - [co saveComponent: todo]; + // resetAlarm=yes is set only when we are about to show the alarm popup in the Web + // interface of SOGo. See generic.js for details. snoozeAlarm=X is called when the + // user clicks on "Snooze for" X minutes, when the popup is being displayed. + // If either is set to yes, we must find the right alarm. + resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue]; + snoozeAlarm = [[[context request] formValueForKey: @"snoozeAlarm"] intValue]; + + if (resetAlarm || snoozeAlarm) + { + iCalToDo *master; + + master = todo; + [[co container] findEntityForClosestAlarm: &todo + timezone: timeZone + startDate: &startDate + endDate: &dueDate]; + if (resetAlarm) + { + iCalTrigger *aTrigger; + + anAlarm = [[todo alarms] objectAtIndex: 0]; + aTrigger = [anAlarm trigger]; + [aTrigger setValue: 0 ofAttribute: @"x-webstatus" to: @"triggered"]; + + [co saveComponent: master]; + } + else if (snoozeAlarm) + { + anAlarm = [[todo alarms] objectAtIndex: 0]; + if ([[anAlarm action] caseInsensitiveCompare: @"DISPLAY"] == NSOrderedSame) + [co snoozeAlarm: snoozeAlarm]; + } } + resetAlarm = [[[context request] formValueForKey: @"resetAlarm"] boolValue]; data = [NSDictionary dictionaryWithObjectsAndKeys: [todo tag], @"component", @@ -538,26 +563,6 @@ } -// TODO: add tentatively - -// - (id) acceptOrDeclineAction: (BOOL) _accept -// { -// [[self clientObject] changeParticipationStatus: -// _accept ? @"ACCEPTED" : @"DECLINED"]; - -// return self; -// } - -// - (id) acceptAction -// { -// return [self acceptOrDeclineAction: YES]; -// } - -// - (id) declineAction -// { -// return [self acceptOrDeclineAction: NO]; -// } - - (id) changeStatusAction { NSString *newStatus; diff --git a/UI/Scheduler/product.plist b/UI/Scheduler/product.plist index aab66ebfc..c13980130 100644 --- a/UI/Scheduler/product.plist +++ b/UI/Scheduler/product.plist @@ -399,6 +399,11 @@ pageName = "UIxTaskEditor"; actionName = "save"; }; + delete = { + protectedBy = "Delete Object"; + pageName = "UIxOccurenceDialog"; + actionName = "delete"; + }; }; }; }; diff --git a/UI/WebServerResources/SchedulerUI.js b/UI/WebServerResources/SchedulerUI.js index e186e8200..789d86fe0 100644 --- a/UI/WebServerResources/SchedulerUI.js +++ b/UI/WebServerResources/SchedulerUI.js @@ -678,10 +678,10 @@ function _deleteEventFromTables(calendar, cname, occurenceTime) { } // Delete task from tasks list - var row = $(calendar + basename); - if (row) { + var rows = $$("tr[id^='" + calendar + basename + "']"); + rows.each(function(row) { row.parentNode.removeChild(row); - } + }); } function _deleteCalendarEventCache(calendar, cname, occurenceTime) { @@ -725,7 +725,6 @@ function _deleteCalendarEventCache(calendar, cname, occurenceTime) { function deleteEventCallback(http) { if (http.readyState == 4) { if (isHttpStatus204(http.status)) { - var isTask = false; var calendar = http.callbackData.calendar; var events = http.callbackData.events; for (var i = 0; i < events.length; i++) { @@ -901,14 +900,14 @@ function performEventDeletion(folder, event, recurrence) { if (recurrence) { // Only one recurrence var occurenceTime = recurrence.substring(9); - var nodes = _eventBlocksMatching(folder, event, occurenceTime); + //var nodes = _eventBlocksMatching(folder, event, occurenceTime); var urlstr = ApplicationBaseURL + "/" + folder + "/" + event + "/" + recurrence + "/delete"; - if (nodes) - document.deleteEventAjaxRequest = triggerAjaxRequest(urlstr, - performDeleteEventCallback, - { nodes: nodes, - occurence: occurenceTime }); + document.deleteEventAjaxRequest = triggerAjaxRequest(urlstr, + performDeleteEventCallback, + { calendar: folder, + cname: event, + occurence: occurenceTime }); } else { // All recurrences @@ -926,11 +925,11 @@ function performEventDeletion(folder, event, recurrence) { function performDeleteEventCallback(http) { if (http.readyState == 4) { if (isHttpStatus204(http.status)) { + var occurenceTime = http.callbackData.occurence; - var nodes = http.callbackData.nodes; - var cname = nodes[0].cname; - var calendar = nodes[0].calendar; - + var cname = http.callbackData.cname; + var calendar = http.callbackData.calendar; + _deleteCalendarEventBlocks(calendar, cname, occurenceTime); _deleteEventFromTables(calendar, cname, occurenceTime); _deleteCalendarEventCache(calendar, cname, occurenceTime); @@ -1178,8 +1177,10 @@ function tasksListCallback(http) { // [10] Erasable? // [11] Priority (0, 1 = important, 9 = low) // [12] Owner - // [13] Status CSS class (duelater, completed, etc) - // [14] Due date (formatted) + // [13] recurrence-id + // [14] isException + // [15] Status CSS class (duelater, completed, etc) + // [16] Due date (formatted) for (var i = 0; i < data.length; i++) { var row = createElement("tr"); @@ -1189,9 +1190,22 @@ function tasksListCallback(http) { var calendar = escape(data[i][1]); var cname = escape(data[i][0]); - row.setAttribute("id", calendar + "-" + cname); + + var rTime = data[i][13]; + var id = escape(data[i][1] + "-" + data[i][0]); + if (rTime) + id += "-" + escape(rTime); + row.setAttribute("id", id); + //row.cname = escape(data[i][0]); + //row.calendar = calendar; + if (rTime) + row.recurrenceTime = escape(rTime); + row.isException = data[i][14]; + + + //row.setAttribute("id", calendar + "-" + cname); //listItem.addClassName(data[i][5]); // Classification - row.addClassName(data[i][14]); // status + //row.addClassName(data[i][14]); // status row.addClassName("taskRow"); row.calendar = calendar; row.cname = cname; @@ -1236,8 +1250,8 @@ function tasksListCallback(http) { cell = createElement("td"); row.appendChild(cell); - if (data[i][14]) - cell.update(data[i][14]); // end date + if (data[i][16]) + cell.update(data[i][16]); // end date cell = createElement("td"); row.appendChild(cell); @@ -3086,7 +3100,12 @@ function marksTasksAsCompleted () { function _updateTaskCompletion (task, value) { url = (ApplicationBaseURL + "/" + task.calendar - + "/" + task.cname + "/changeStatus?status=" + value); + + "/" + task.cname); + + if (task.recurrenceTime) + url += ("/occurence" + task.recurrenceTime); + + url += ("/changeStatus?status=" + value); triggerAjaxRequest(url, refreshTasks, null); diff --git a/UI/WebServerResources/UIxTaskEditor.js b/UI/WebServerResources/UIxTaskEditor.js index 38e0d0d30..6fa0aa649 100644 --- a/UI/WebServerResources/UIxTaskEditor.js +++ b/UI/WebServerResources/UIxTaskEditor.js @@ -110,8 +110,10 @@ function onTimeControlCheck(checkBox) { for (var i = 0; i < selects.length; i++) if (selects[i] != checkBox) selects[i].disabled = !checkBox.checked; - if (checkBox.id == "startDateCB") + if (checkBox.id == "startDateCB") { + $("repeatList").disabled = !checkBox.checked; $("reminderList").disabled = !checkBox.checked; + } } }